[IMP] product, website_sale(comparison): enable product tags and ribbons

use product tags to display information as color/image on the product
page, enable filtering by tags, add tags to specification/comparison
table. Remove the link between ribbons and tags because there is no use
as we are able to show the tags on the page, add ribbon to
product.product

task-3299538

closes odoo/odoo#124299

Related: odoo/upgrade#4824
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Valeriya(vchu)
2023-07-20 10:06:47 +02:00
committed by Antoine Vandevenne (anv)
parent be3955d1de
commit bc3026e2b2
20 changed files with 320 additions and 66 deletions
+2 -3
View File
@@ -32,8 +32,8 @@ Print product labels with barcode.
'security/ir.model.access.csv',
'wizard/product_label_layout_views.xml',
'views/product_views.xml',
'views/product_tag_views.xml',
'views/product_views.xml', # To keep after product_tag_views.xml because it depends on it.
'views/res_config_settings_views.xml',
'views/product_attribute_views.xml',
@@ -43,7 +43,6 @@ Print product labels with barcode.
'views/product_pricelist_views.xml',
'views/product_supplierinfo_views.xml',
'views/product_template_views.xml',
'views/product_tag_views.xml',
'views/res_country_group_views.xml',
'views/res_partner_views.xml',
+6 -1
View File
@@ -64,7 +64,12 @@ class ProductProduct(models.Model):
'product.packaging', 'product_id', 'Product Packages',
help="Gives the different ways to package the same product.")
additional_product_tag_ids = fields.Many2many('product.tag', 'product_tag_product_product_rel')
additional_product_tag_ids = fields.Many2many(
string="Additional Product Tags",
comodel_name='product.tag',
relation='product_tag_product_product_rel',
domain="[('id', 'not in', product_tag_ids)]",
)
all_product_tag_ids = fields.Many2many('product.tag', compute='_compute_all_product_tag_ids', search='_search_all_product_tag_ids')
# all image fields are base64 encoded and PIL-supported
+27 -8
View File
@@ -1,22 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.osv import expression
class ProductTag(models.Model):
_name = 'product.tag'
_description = 'Product Tag'
def _get_default_color(self):
return randint(1, 11)
def _get_default_template_id(self):
return self.env['product.template'].browse(self.env.context.get('product_template_id'))
name = fields.Char('Tag Name', required=True, translate=True)
color = fields.Integer('Color', default=_get_default_color)
def _get_default_variant_id(self):
return self.env['product.product'].browse(self.env.context.get('product_variant_id'))
product_template_ids = fields.Many2many('product.template', 'product_tag_product_template_rel')
product_product_ids = fields.Many2many('product.product', 'product_tag_product_product_rel')
name = fields.Char(string="Name", required=True, translate=True)
color = fields.Char(string="Color", default='#3C3C3C')
product_template_ids = fields.Many2many(
string="Product Templates",
comodel_name='product.template',
relation='product_tag_product_template_rel',
default=_get_default_template_id,
)
product_product_ids = fields.Many2many(
string="Product Variants",
comodel_name='product.product',
relation='product_tag_product_product_rel',
domain="[('attribute_line_ids', '!=', False), ('product_tmpl_id', 'not in', product_template_ids)]",
default=_get_default_variant_id,
)
product_ids = fields.Many2many(
'product.product', string='All Product Variants using this Tag',
compute='_compute_product_ids', search='_search_product_ids'
@@ -31,6 +43,13 @@ class ProductTag(models.Model):
for tag in self:
tag.product_ids = tag.product_template_ids.product_variant_ids | tag.product_product_ids
@api.returns(None, lambda value: value[0])
def copy_data(self, default=None):
default = default or {}
if not default.get('name'):
default['name'] = _('%s (copy)', self.name)
return super().copy_data(default=default)
def _search_product_ids(self, operator, operand):
if operator in expression.NEGATIVE_TERM_OPERATORS:
return [('product_template_ids.product_variant_ids', operator, operand), ('product_product_ids', operator, operand)]
+5 -1
View File
@@ -143,7 +143,11 @@ class ProductTemplate(models.Model):
('1', 'Favorite'),
], default='0', string="Favorite")
product_tag_ids = fields.Many2many('product.tag', 'product_tag_product_template_rel', string='Product Tags')
product_tag_ids = fields.Many2many(
string="Product Template Tags",
comodel_name='product.tag',
relation='product_tag_product_template_rel',
)
def _compute_item_count(self):
for template in self:
+12 -5
View File
@@ -10,12 +10,16 @@
<group>
<group>
<field name="name"/>
<field name="color" widget="color_picker"/>
<field name="color" widget="color"/>
</group>
</group>
<group>
<field name="product_ids" widget="many2many_tags"
attrs="{'invisible':[('product_ids','=',[])]}"/>
<field name="product_template_ids" widget="many2many_tags"/>
<field name="product_product_ids" widget="many2many_tags"/>
<field name="product_ids"
widget="many2many_tags"
attrs="{'invisible': [('product_ids', '=', [])]}"
/>
</group>
</sheet>
</form>
@@ -25,9 +29,11 @@
<field name="name">product.tag.tree</field>
<field name="model">product.tag</field>
<field name="arch" type="xml">
<tree string="Product Tags">
<tree string="Product Tags" multi_edit="1">
<field name="name"/>
<field name="color" widget="color_picker"/>
<field name="product_template_ids" widget="many2many_tags" optional="show"/>
<field name="product_product_ids" widget="many2many_tags" string="Product Variant" optional="show"/>
<field name="color" widget="color" readonly="1" optional="hide"/>
</tree>
</field>
</record>
@@ -36,6 +42,7 @@
<field name="res_model">product.tag</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False"/>
<field name="context">{'create': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Define a new tag
@@ -15,7 +15,7 @@
<field name="priority" widget="priority" optional="show" nolabel="1"/>
<field name="name" string="Product Name"/>
<field name="default_code" optional="show"/>
<field name="product_tag_ids" string="Tags" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="product_tag_ids" widget="many2many_tags" optional="show"/>
<field name="barcode" optional="hide" attrs="{'readonly': [('product_variant_count', '!=', 1)]}"/>
<field name="company_id" options="{'no_create': True}"
groups="base.group_multi_company" optional="hide"/>
+14 -5
View File
@@ -82,7 +82,11 @@
</span>
</div>
<field name="categ_id" string="Product Category"/>
<field name="product_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<label for="product_tag_ids"/>
<div>
<field name="product_tag_ids" widget="many2many_tags" class="oe_inline" context="{'product_template_id': id}"/>
<button name="%(product_tag_action)d" icon="oi-arrow-right" title="Tags" type="action" class="btn-link"/>
</div>
<field name="company_id" groups="base.group_multi_company"
options="{'no_create': True}"/>
<field name="currency_id" invisible="1"/>
@@ -283,8 +287,12 @@
</div>
</group>
<group name="tags" string="Tags">
<field name="product_tag_ids" string="Product Template Tags" widget="many2many_tags" readonly="1" options="{'no_open': True, 'color_field': 'color'}"/>
<field name="additional_product_tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': 1}"/>
<field name="product_tag_ids" widget="many2many_tags" readonly="1"/>
<label for="additional_product_tag_ids"/>
<div>
<field name="additional_product_tag_ids" widget="many2many_tags" class="oe_inline" context="{'product_variant_id': id}"/>
<button name="%(product_tag_action)d" icon="oi-arrow-right" title="Tags" type="action" class="btn-link"/>
</div>
</group>
</group>
<group>
@@ -338,7 +346,8 @@
<field name="lst_price" optional="show" string="Sales Price"/>
<field name="standard_price" optional="show"/>
<field name="categ_id" optional="hide"/>
<field name="product_tag_ids" string="Tags" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': 1}" optional="hide"/>
<field name="product_tag_ids" widget="many2many_tags" readonly="1" optional="hide"/>
<field name="additional_product_tag_ids" widget="many2many_tags" optional="hide"/>
<field name="type" optional="hide" readonly="1"/>
<field name="uom_id" string="Unit" groups="uom.group_uom" optional="show" readonly="1"/>
<field name="product_tmpl_id" invisible="1" readonly="1"/>
@@ -388,7 +397,7 @@
<attribute name="options">{'no_open': true}</attribute>
</field>
<field name="product_tag_ids" position="after">
<field name="additional_product_tag_ids" options="{'color_field': 'color', 'no_edit_color': 1}"/>
<field name="additional_product_tag_ids"/>
</field>
</field>
</record>
+30 -5
View File
@@ -81,7 +81,7 @@ class TableCompute(object):
self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False
self.table[pos // ppr][pos % ppr] = {
'product': p, 'x': x, 'y': y,
'ribbon': p.sudo()._get_website_ribbon(),
'ribbon': p.sudo().website_ribbon_id,
}
if index <= ppg:
maxy = max(maxy, y + (pos // ppr))
@@ -225,7 +225,8 @@ class WebsiteSale(http.Controller):
yield {'loc': loc}
def _get_search_options(
self, category=None, attrib_values=None, min_price=0.0, max_price=0.0, conversion_rate=1, **post
self, category=None, attrib_values=None, tags=None, min_price=0.0, max_price=0.0,
conversion_rate=1, **post
):
return {
'displayDescription': True,
@@ -235,6 +236,7 @@ class WebsiteSale(http.Controller):
'displayImage': True,
'allowFuzzy': not post.get('noFuzzy'),
'category': str(category.id) if category else None,
'tags': tags,
'min_price': min_price / conversion_rate,
'max_price': max_price / conversion_rate,
'attrib_values': attrib_values,
@@ -251,11 +253,14 @@ class WebsiteSale(http.Controller):
return fuzzy_search_term, product_count, search_result
def _shop_get_query_url_kwargs(self, category, search, min_price, max_price, attrib=None, order=None, **post):
def _shop_get_query_url_kwargs(
self, category, search, min_price, max_price, attrib=None, order=None, tags=None, **post
):
return {
'category': category,
'search': search,
'attrib': attrib,
'tags': tags,
'min_price': min_price,
'max_price': max_price,
'order': order,
@@ -291,6 +296,7 @@ class WebsiteSale(http.Controller):
category = Category
website = request.env['website'].get_current_website()
website_domain = website.website_domain()
if ppg:
try:
ppg = int(ppg)
@@ -302,11 +308,28 @@ class WebsiteSale(http.Controller):
ppr = website.shop_ppr or 4
attrib_list = request.httprequest.args.getlist('attrib')
request_args = request.httprequest.args
attrib_list = request_args.getlist('attrib')
attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v]
attributes_ids = {v[0] for v in attrib_values}
attrib_set = {v[1] for v in attrib_values}
filter_by_tags_enabled = website.is_view_active('website_sale.filter_products_tags')
if filter_by_tags_enabled:
tags = request_args.getlist('tags')
# Allow only numeric tag values to avoid internal error.
if tags and all(tag.isnumeric() for tag in tags):
post['tags'] = tags
tags = {int(tag) for tag in tags}
else:
post['tags'] = None
tags = {}
ProductTag = request.env['product.tag']
all_tags = ProductTag.search(
[('product_ids.is_published', '=', True), ('visible_on_ecommerce', '=', True)]
+ website_domain
)
keep = QueryURL('/shop', **self._shop_get_query_url_kwargs(category and int(category), search, min_price, max_price, **post))
now = datetime.timestamp(datetime.now())
@@ -375,7 +398,6 @@ class WebsiteSale(http.Controller):
max_price = max_price if max_price >= available_min_price else available_max_price
post['max_price'] = max_price
website_domain = website.website_domain()
categs_domain = [('parent_id', '=', False)] + website_domain
if search:
search_categories = Category.search(
@@ -443,6 +465,8 @@ class WebsiteSale(http.Controller):
values['max_price'] = max_price or available_max_price
values['available_min_price'] = tools.float_round(available_min_price, 2)
values['available_max_price'] = tools.float_round(available_max_price, 2)
if filter_by_tags_enabled:
values.update({'all_tags': all_tags, 'tags': tags})
if category:
values['main_object'] = category
values.update(self._get_additional_shop_values(values))
@@ -606,6 +630,7 @@ class WebsiteSale(http.Controller):
'category': category,
'search': search,
'attrib': attrib,
'tags': kwargs.get('tags'),
'min_price': kwargs.get('min_price'),
'max_price': kwargs.get('max_price'),
}
@@ -13,6 +13,7 @@ class Product(models.Model):
product_variant_image_ids = fields.One2many('product.image', 'product_variant_id', string="Extra Variant Images")
website_url = fields.Char('Website URL', compute='_compute_product_website_url', help='The full URL to access the document through the website.')
ribbon_id = fields.Many2one(string="Variant Ribbon", comodel_name='product.ribbon')
base_unit_count = fields.Float('Base Unit Count', required=True, default=1, help="Display base unit price on your eCommerce pages. Set to 0 to hide it for this product.")
base_unit_id = fields.Many2one('website.base.unit', string='Custom Unit of Measure', help="Define a custom unit to display in the price per unit of measure field.")
@@ -17,4 +17,3 @@ class ProductRibbon(models.Model):
bg_color = fields.Char(string='Ribbon background color', required=False)
text_color = fields.Char(string='Ribbon text color', required=False)
html_class = fields.Char(string='Ribbon class', required=True, default='')
product_tag_ids = fields.One2many('product.tag', 'ribbon_id', string='Product Tags')
+6 -1
View File
@@ -7,4 +7,9 @@ class ProductTag(models.Model):
_name = 'product.tag'
_inherit = ['website.multi.mixin', 'product.tag']
ribbon_id = fields.Many2one('product.ribbon', string='Ribbon')
visible_on_ecommerce = fields.Boolean(
string="Visible on eCommerce",
help="Whether the tag is displayed on the eCommerce.",
default=True,
)
image = fields.Image(string="Image", max_width=50, max_height=50)
+14 -9
View File
@@ -241,10 +241,9 @@ class ProductTemplate(models.Model):
"""
self.ensure_one()
current_website = False
current_website = self.env['website'].get_current_website()
if self.env.context.get('website_id'):
current_website = self.env['website'].get_current_website()
if not pricelist:
pricelist = current_website.get_current_pricelist()
@@ -252,8 +251,17 @@ class ProductTemplate(models.Model):
combination=combination, product_id=product_id, add_qty=add_qty, pricelist=pricelist,
parent_combination=parent_combination, only_template=only_template)
product = self.env['product.product'].browse(combination_info['product_id'])
if current_website.is_view_active('website_sale.product_tags'):
combination_info['product_tags'] = self.env['ir.ui.view']._render_template(
'website_sale.product_tags', values={
'all_product_tags': product.all_product_tag_ids.filtered('visible_on_ecommerce')
}
)
if self.env.context.get('website_id'):
product = self.env['product.product'].browse(combination_info['product_id']) or self
product = product or self
partner = self.env.user.partner_id
company_id = current_website.company_id
currency = current_website.currency_id
@@ -402,12 +410,6 @@ class ProductTemplate(models.Model):
if product.id:
product.website_url = "/shop/%s" % slug(product)
def _get_website_ribbon(self):
if self.website_ribbon_id:
return self.website_ribbon_id
return self.product_tag_ids.ribbon_id[:1] or self.product_variant_ids.additional_product_tag_ids.ribbon_id[:1]
@api.model
def _get_alternative_product_filter(self):
return self.env.ref('website_sale.dynamic_filter_cross_selling_alternative_products').id
@@ -442,11 +444,14 @@ class ProductTemplate(models.Model):
with_price = options['displayDetail']
domains = [website.sale_product_domain()]
category = options.get('category')
tags = options.get('tags')
min_price = options.get('min_price')
max_price = options.get('max_price')
attrib_values = options.get('attrib_values')
if category:
domains.append([('public_categ_ids', 'child_of', unslug(category)[1])])
if tags:
domains.append([('product_variant_ids.all_product_tag_ids', 'in', tags)])
if min_price:
domains.append([('list_price', '>=', min_price)])
if max_price:
@@ -600,6 +600,11 @@ var VariantMixin = {
.text(combination.price)
.trigger('change');
$parent
.find('.o_product_tags')
.first()
.html(combination.product_tags);
this.handleCustomValues($(ev.target));
},
@@ -755,3 +755,7 @@ a.no-decoration {
border-radius: 50%;
}
}
.o_product_tag_img {
height: 2rem;
}
@@ -7,8 +7,13 @@
<field name="model">product.tag</field>
<field name="inherit_id" ref="product.product_tag_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='color']/.." position="after">
<group>
<field name="visible_on_ecommerce"/>
</group>
</xpath>
<field name="color" position="after">
<field name="ribbon_id"/>
<field name="image" widget="image" options="{'size': [0, 55]}"/>
</field>
</field>
</record>
@@ -19,16 +24,12 @@
<field name="inherit_id" ref="product.product_tag_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="ribbon_id"/>
<field name="visible_on_ecommerce" optional="show"/>
</field>
<field name="color" position="after">
<field name="image" widget="image" options="{'size': [0, 30]}" optional="hide"/>
</field>
</field>
</record>
<record id="product_public_tags_action" model="ir.actions.act_window">
<field name="name">Product Tags</field>
<field name="res_model">product.tag</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False"/>
</record>
</odoo>
+6 -5
View File
@@ -22,6 +22,9 @@
<field name="website_id" groups="website.group_multi_website" optional="show"/>
<field name="is_published" string="Is Published" optional="hide"/>
</field>
<field name="additional_product_tag_ids" position="after">
<field name="ribbon_id" options="{'no_quick_create': True}" optional="hide"/>
</field>
</field>
</record>
@@ -188,6 +191,9 @@
<group name="product_variant_images" string="Extra Variant Media">
<field name="product_variant_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" add-label="Add a Media" nolabel="1"/>
</group>
<group name="sales" string="Sales" groups="base.group_no_one">
<field name="ribbon_id" options="{'no_quick_create': True}"/>
</group>
</group>
</field>
</record>
@@ -209,11 +215,6 @@
<field name="bg_color"/>
</group>
</group>
<group string="Products">
<group>
<field name="product_tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': 1}"/>
</group>
</group>
</sheet>
</form>
</field>
@@ -120,6 +120,13 @@
data-dependencies="attributes_opt, attributes_opt_top"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Product Tags Filter"
class="o_we_sublevel_1"
data-customize-website-views="website_sale.filter_products_tags"
data-dependencies="attributes_opt, attributes_opt_top"
data-no-preview="true"
data-reload="/"
/>
<we-row string="Top Bar" class="o_we_full_row">
<we-button string="Sort by"
data-customize-website-views="website_sale.sort"
@@ -184,7 +191,7 @@
<we-input string="Content" class="o_we_sublevel_1 o_we_large"
data-set-ribbon-html="Badge Text" data-apply-to=".o_ribbon"/>
<we-colorpicker string="Background" class="o_we_sublevel_1"
title="" data-select-style="" data-css-property="background-color" data-color-prefix="bg-" data-apply-to=".o_ribbon"/>
title="" data-select-style="" data-css-property="background-color" data-color-prefix="text-bg-" data-apply-to=".o_ribbon"/>
<we-colorpicker string="Text" class="o_we_sublevel_1"
title="" data-select-style="" data-css-property="color" data-apply-to=".o_ribbon"/>
<we-select string="Style" class="o_we_sublevel_1">
@@ -242,6 +249,11 @@
<we-button data-name="variants_options_opt" data-customize-website-views="">Options</we-button>
<we-button data-name="variants_products_list_opt" data-customize-website-views="website_sale.product_variants">Products List</we-button>
</we-select>
<we-checkbox string="Product Tags"
data-customize-website-views="website_sale.product_tags"
data-no-preview="true"
data-reload="/"
/>
<we-row string="Cart" class="o_we_full_row" data-name="o_wsale_buy_now_opt">
<we-button title="Buy Now" class="o_we_buy_now_btn"
data-customize-website-views="website_sale.product_buy_now"
+124 -9
View File
@@ -275,6 +275,7 @@
<t t-set="opt_wsale_categories" t-value="is_view_active('website_sale.products_categories')"/>
<t t-set="opt_wsale_attributes" t-value="is_view_active('website_sale.products_attributes')"/>
<t t-set="opt_wsale_filter_price" t-value="is_view_active('website_sale.filter_products_price')"/>
<t t-set="opt_wsale_filter_tags" t-value="is_view_active('website_sale.filter_products_tags')"/>
<t t-set="opt_wsale_categories_top" t-value="is_view_active('website_sale.products_categories_top')"/>
<t t-set="opt_wsale_attributes_top" t-value="is_view_active('website_sale.products_attributes_top')"/>
@@ -340,7 +341,7 @@
data-bs-toggle="offcanvas"
data-bs-target="#o_wsale_offcanvas">
<i class="fa fa-sliders"/>
<span t-if="isFilteringByPrice or attrib_set" t-attf-class="position-absolute top-0 start-100 translate-middle badge border border-{{navClass}} rounded-circle bg-danger p-1"><span class="visually-hidden">filters active</span></span>
<span t-if="isFilteringByPrice or attrib_set or tags" t-attf-class="position-absolute top-0 start-100 translate-middle badge border border-{{navClass}} rounded-circle bg-danger p-1"><span class="visually-hidden">filters active</span></span>
</button>
</div>
@@ -649,6 +650,37 @@
</div>
</div>
</t>
<t t-if="opt_wsale_filter_tags and (opt_wsale_attributes or opt_wsale_attributes_top)">
<t t-set="_status" t-value="'inactive'"/>
<t t-foreach="all_tags" t-as="v" t-if="v.id in tags" t-set="_status" t-value="'active'"/>
<div t-if="all_tags">
<h2 class="accordion-header mb-0" t-attf-id="o_wsale_offcanvas_tags_header">
<button t-attf-class="o_wsale_offcanvas_title accordion-button border-top rounded-0 {{ not tags and 'collapsed'}}"
type="button"
t-att-data-status="_status"
data-bs-toggle="collapse"
t-attf-data-bs-target="#o_wsale_offcanvas_tags"
t-att-aria-expanded="_status == 'active' and 'True' or 'False'"
t-attf-aria-controls="o_wsale_offcanvas_tags"
>
<b>Tags</b>
</button>
</h2>
<div t-attf-id="o_wsale_offcanvas_tags"
t-attf-class="accordion-collapse collapse {{ (_status == 'active') and 'show'}}"
t-att-aria-expanded="(_status == 'active') and 'True' or 'False'"
t-attf-aria-labelledby="o_wsale_offcanvas_tags_header"
>
<div class="accordion-body pt-0">
<div class="list-group list-group-flush">
<t t-call="website_sale.filter_products_tags_list">
<t t-set="all_tags" t-value="all_tags"/>
</t>
</div>
</div>
</div>
</div>
</t>
</form>
<t t-if="opt_wsale_filter_price and (opt_wsale_attributes or opt_wsale_attributes_top)"
@@ -658,8 +690,8 @@
</t>
</div>
<div class="offcanvas-body d-flex justify-content-between flex-grow-0 border-top overflow-hidden">
<a t-attf-class="btn btn-{{navClass}} d-flex py-1 mb-2 {{(not attrib_values and not isFilteringByPrice) and 'disabled' }}"
t-att-aria-disabled="(not attrib_values and not isFilteringByPrice) and 'true' or 'false'"
<a t-attf-class="btn btn-{{navClass}} d-flex py-1 mb-2 {{(not attrib_values and not isFilteringByPrice and not tags) and 'disabled' }}"
t-att-aria-disabled="(not attrib_values and not isFilteringByPrice and not tags) and 'true' or 'false'"
href="/shop"
title="Clear Filters">
Clear Filters
@@ -800,11 +832,11 @@
<xpath expr="//div[hasclass('products_attributes_filters')]" position="inside">
<div id="wsale_products_attributes_collapse"
class=" position-relative">
<form t-if="attributes" class="js_attributes position-relative mb-2" method="get">
<form t-if="attributes or all_tags" class="js_attributes position-relative mb-2" method="get">
<input t-if="category" type="hidden" name="category" t-att-value="category.id" />
<input type="hidden" name="search" t-att-value="search" />
<input type="hidden" name="order" t-att-value="order"/>
<a t-if="attrib_values" t-att-href="keep('/shop'+ ('/category/'+slug(category)) if category else None, attrib=0)" t-attf-class="btn btn-{{navClass}} d-flex py-1 mb-2">
<a t-if="attrib_values or tags" t-att-href="keep('/shop'+ ('/category/'+slug(category)) if category else None, attrib=0, tags=0)" t-attf-class="btn btn-{{navClass}} d-flex py-1 mb-2">
<small class="mx-auto"><b>Clear Filters</b></small>
<i class="oi oi-close"/>
</a>
@@ -842,6 +874,11 @@
</div>
</div>
</t>
<t t-if="opt_wsale_filter_tags and opt_wsale_attributes"
t-call="website_sale.filter_products_tags"
>
<t t-set="all_tags" t-value="all_tags"/>
</t>
</form>
</div>
</xpath>
@@ -863,6 +900,34 @@
</div>
</template>
<template id="filter_products_tags" name="Filter by Tags" active="True">
<div t-if="all_tags">
<h6 class="mb-3">
<b>Tags</b>
</h6>
<div class="flex-column mb-3">
<t t-call="website_sale.filter_products_tags_list">
<t t-set="all_tags" t-value="all_tags"/>
</t>
</div>
</div>
</template>
<template id="filter_products_tags_list">
<t t-foreach="all_tags" t-as="tag" class="list-group-item border-0 ps-0 pb-0">
<div class="form-check mb-1">
<input type="checkbox"
name="tags"
class="form-check-input"
t-att-id="tag.id"
t-att-value="tag.id"
t-att-checked="'checked' if tag.id in tags else None"
/>
<label class="form-check-label fw-normal" t-att-for="tag.id" t-field="tag.name"/>
</div>
</t>
</template>
<template id="products_list_view" inherit_id="website_sale.products" active="False" name="List View (by default)">
<xpath expr="//div[@id='products_grid']" position="after">
<!-- Nothing to do, this view is only meant to allow the server -->
@@ -959,7 +1024,6 @@
<input type="hidden" class="product_id" name="product_id" t-att-value="product_variant.id" />
<input type="hidden" class="product_template_id" name="product_template_id" t-att-value="product.id" />
<input t-if="product.public_categ_ids.ids" type="hidden" class="product_category_id" name="product_category_id" t-att-value="product.public_categ_ids.ids[0]" />
<input t-if="product.product_tag_ids.ids" type="hidden" class="product_tag_id" name="product_tag_id" t-att-value="product.product_tag_ids.ids[0]" />
<t t-if="combination" t-call="website_sale.variants">
<t t-set="ul_class" t-valuef="flex-column" />
<t t-set="parent_combination" t-value="None" />
@@ -992,6 +1056,9 @@
</div>
</section>
</div>
<t t-if="is_view_active('website_sale.product_tags')" t-call="website_sale.product_tags">
<t t-set="all_product_tags" t-value="product_variant.all_product_tag_ids"/>
</t>
</div>
</form>
<p t-elif="not product.active" class="alert alert-warning">This product is no longer available.</p>
@@ -1023,6 +1090,29 @@
</t>
</template>
<template id="product_tags" name="Product Tags" active="True">
<div class="o_product_tags o_field_tags d-flex flex-wrap align-items-center gap-2 mb16 mt-2">
<t t-foreach="all_product_tags" t-as="tag">
<t t-if="tag.visible_on_ecommerce">
<span t-if="tag.image"
class="order-0"
t-field="tag.image"
t-options="{'widget': 'image', 'class': 'o_product_tag_img rounded'}"
/>
<span t-else="" class="position-relative order-1 py-1 px-2">
<span class="position-absolute top-0 start-0 w-100 h-100 rounded"
t-attf-style="background-color: #{tag.color}; opacity: .2;"
/>
<span class="text-nowrap small"
t-attf-style="color: #{tag.color}"
t-field="tag.name"
/>
</span>
</t>
</t>
</div>
</template>
<template id="alternative_products" name="Alternative Products" inherit_id="website_sale.product" active="True">
<div itemprop="description" position="after">
<div class="oe_structure oe_structure_solo oe_unremovable oe_unmovable" id="oe_structure_website_sale_recommended_products" t-ignore="true" t-if="product.alternative_product_ids">
@@ -2357,6 +2447,10 @@
<!-- 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="ribbon" t-value="product_variant.sudo().ribbon_id or product.sudo().website_ribbon_id"/>
<t t-set="bg_color" t-value="ribbon['bg_color'] or ''"/>
<t t-set="text_color" t-value="ribbon['text_color']"/>
<t t-set="bg_class" t-value="ribbon['html_class']"/>
<t t-call="website_sale.shop_product_#{website.product_page_image_layout}"/>
</template>
@@ -2364,6 +2458,18 @@
<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-elif="len(product_images) == 1 and website.product_page_image_layout != 'grid'"
class="position-relative d-inline-flex overflow-hidden m-auto h-100"
>
<span t-attf-class="o_ribbon #{ribbon['html_class']} z-index-1"
t-attf-style="#{text_color and ('color: %s; ' % text_color)}#{bg_color and 'background-color:' + bg_color}"
t-out="ribbon['html'] or ''"
/>
<div t-field="product_image.image_1920"
class="d-flex align-items-start justify-content-center h-100 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"}'
/>
</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"}'/>
</template>
@@ -2371,11 +2477,16 @@
<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="o_carousel_product_outer carousel-outer position-relative flex-grow-1 overflow-hidden">
<span t-if="len(product_images) > 1"
t-attf-class="o_ribbon #{ribbon['html_class']} z-index-1"
t-attf-style="#{text_color and ('color: %s; ' % text_color)}#{bg_color and 'background-color:' + bg_color}"
t-out="ribbon['html'] or ''"
/>
<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-attf-class="carousel-item h-100 text-center#{' active' if product_image_first else ''}">
<t t-call="website_sale.shop_product_image"/>
</div>
</t>
@@ -2428,7 +2539,11 @@
<!-- 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">
<div class="container position-relative overflow-hidden">
<span t-attf-class="o_ribbon #{ribbon['html_class']}"
t-attf-style="#{text_color and ('color: %s; ' % text_color)}#{bg_color and 'background-color:' + bg_color}"
t-out="ribbon['html'] or ''"
/>
<!-- 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()"/>
@@ -52,8 +52,7 @@
sequence="5"/>
<menuitem id="product_catalog_product_tags"
name="Product Tags"
action="product_public_tags_action"
groups="base.group_no_one"/>
action="product.product_tag_action"/>
</menuitem>
</menuitem>
@@ -56,6 +56,27 @@
</table>
</div>
</t>
<t t-if="is_view_active('website_sale.product_tags')">
<div class="col-lg-6">
<table class="table">
<t t-if="product.product_variant_ids.all_product_tag_ids">
<tr>
<th class="text-start" t-att-colspan="2">
<span>Tags</span>
</th>
</tr>
<tr>
<td class="w-25"><span>Tags</span></td>
<td class="w-75 text-muted">
<t t-call="website_sale.product_tags">
<t t-set="all_product_tags" t-value="product.product_variant_ids.all_product_tag_ids"/>
</t>
</td>
</tr>
</t>
</table>
</div>
</t>
</div>
</div>
</div>
@@ -140,6 +161,24 @@
</td>
</tr>
</t>
<t t-if="is_view_active('website_sale.product_tags') and any([product.all_product_tag_ids for product in products])">
<tr class="clickable" data-bs-toggle="collapse" data-bs-target=".o_ws_tags">
<th class="text-start" t-att-colspan="len(products) + 1">
<i class="fa fa-chevron-circle-down o_product_comparison_collpase" role="img" aria-label="Collapse" title="Collapse"></i><span>Tags</span>
</th>
</tr>
<tr class="collapse show o_ws_tags">
<td><span>Tags</span></td>
<td t-foreach="products" t-as="product">
<div class="d-flex justify-content-center">
<t t-call="website_sale.product_tags">
<t t-set="all_product_tags" t-value="product.all_product_tag_ids"/>
</t>
</div>
</td>
</tr>
</t>
</tbody>
</table>
</section>