[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:
committed by
Antoine Vandevenne (anv)
parent
be3955d1de
commit
bc3026e2b2
@@ -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',
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user