From bc3026e2b27f985a8633011219b672268b093339 Mon Sep 17 00:00:00 2001 From: "Valeriya(vchu)" Date: Thu, 8 Jun 2023 13:05:38 +0000 Subject: [PATCH] [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) --- addons/product/__manifest__.py | 5 +- addons/product/models/product_product.py | 7 +- addons/product/models/product_tag.py | 35 +++-- addons/product/models/product_template.py | 6 +- addons/product/views/product_tag_views.xml | 17 ++- .../product/views/product_template_views.xml | 2 +- addons/product/views/product_views.xml | 19 ++- addons/website_sale/controllers/main.py | 35 ++++- addons/website_sale/models/product_product.py | 1 + addons/website_sale/models/product_ribbon.py | 1 - addons/website_sale/models/product_tag.py | 7 +- .../website_sale/models/product_template.py | 23 +-- .../static/src/js/sale_variant_mixin.js | 5 + .../static/src/scss/website_sale.scss | 4 + .../website_sale/views/product_tag_views.xml | 19 +-- addons/website_sale/views/product_views.xml | 11 +- .../website_sale/views/snippets/snippets.xml | 14 +- addons/website_sale/views/templates.xml | 133 ++++++++++++++++-- .../website_sale/views/website_sale_menus.xml | 3 +- .../website_sale_comparison_template.xml | 39 +++++ 20 files changed, 320 insertions(+), 66 deletions(-) diff --git a/addons/product/__manifest__.py b/addons/product/__manifest__.py index 8110888402a..b83af700149 100644 --- a/addons/product/__manifest__.py +++ b/addons/product/__manifest__.py @@ -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', diff --git a/addons/product/models/product_product.py b/addons/product/models/product_product.py index a65385b74e5..5fa023607c2 100644 --- a/addons/product/models/product_product.py +++ b/addons/product/models/product_product.py @@ -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 diff --git a/addons/product/models/product_tag.py b/addons/product/models/product_tag.py index 278c26b73d3..71251e6e273 100644 --- a/addons/product/models/product_tag.py +++ b/addons/product/models/product_tag.py @@ -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)] diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index e9e26c63822..755e182b7da 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -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: diff --git a/addons/product/views/product_tag_views.xml b/addons/product/views/product_tag_views.xml index 00791f655ba..c4d3c1b8103 100644 --- a/addons/product/views/product_tag_views.xml +++ b/addons/product/views/product_tag_views.xml @@ -10,12 +10,16 @@ - + - + + + @@ -25,9 +29,11 @@ product.tag.tree product.tag - + - + + + @@ -36,6 +42,7 @@ product.tag tree,form + {'create': True}

Define a new tag diff --git a/addons/product/views/product_template_views.xml b/addons/product/views/product_template_views.xml index 4c49b7abcad..7bb76d0ce9d 100644 --- a/addons/product/views/product_template_views.xml +++ b/addons/product/views/product_template_views.xml @@ -15,7 +15,7 @@ - + diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index f10e997e767..f11c1d42394 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -82,7 +82,11 @@ - +

+ +
@@ -283,8 +287,12 @@ - - + + @@ -338,7 +346,8 @@ - + + @@ -388,7 +397,7 @@ {'no_open': true} - + diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 722e0ead5a8..d3d2b39403a 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -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'), } diff --git a/addons/website_sale/models/product_product.py b/addons/website_sale/models/product_product.py index 969888268ce..36036a6460f 100644 --- a/addons/website_sale/models/product_product.py +++ b/addons/website_sale/models/product_product.py @@ -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.") diff --git a/addons/website_sale/models/product_ribbon.py b/addons/website_sale/models/product_ribbon.py index 71b59523e38..54424354fdd 100644 --- a/addons/website_sale/models/product_ribbon.py +++ b/addons/website_sale/models/product_ribbon.py @@ -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') diff --git a/addons/website_sale/models/product_tag.py b/addons/website_sale/models/product_tag.py index 24556828b89..ad6749f16f0 100644 --- a/addons/website_sale/models/product_tag.py +++ b/addons/website_sale/models/product_tag.py @@ -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) diff --git a/addons/website_sale/models/product_template.py b/addons/website_sale/models/product_template.py index f4b4e8d9f29..3b2adc492ca 100644 --- a/addons/website_sale/models/product_template.py +++ b/addons/website_sale/models/product_template.py @@ -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: diff --git a/addons/website_sale/static/src/js/sale_variant_mixin.js b/addons/website_sale/static/src/js/sale_variant_mixin.js index c97c552c3d1..30c8bbb58f4 100644 --- a/addons/website_sale/static/src/js/sale_variant_mixin.js +++ b/addons/website_sale/static/src/js/sale_variant_mixin.js @@ -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)); }, diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss index 59cbde388fe..d9fcc30df40 100644 --- a/addons/website_sale/static/src/scss/website_sale.scss +++ b/addons/website_sale/static/src/scss/website_sale.scss @@ -755,3 +755,7 @@ a.no-decoration { border-radius: 50%; } } + +.o_product_tag_img { + height: 2rem; +} diff --git a/addons/website_sale/views/product_tag_views.xml b/addons/website_sale/views/product_tag_views.xml index 210525e08b2..9569aa80870 100644 --- a/addons/website_sale/views/product_tag_views.xml +++ b/addons/website_sale/views/product_tag_views.xml @@ -7,8 +7,13 @@ product.tag + + + + + - + @@ -19,16 +24,12 @@ - + + + + - - Product Tags - product.tag - tree,form - - - diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml index 31605478600..31ac8626256 100644 --- a/addons/website_sale/views/product_views.xml +++ b/addons/website_sale/views/product_views.xml @@ -22,6 +22,9 @@ + + + @@ -188,6 +191,9 @@ + + + @@ -209,11 +215,6 @@ - - - - - diff --git a/addons/website_sale/views/snippets/snippets.xml b/addons/website_sale/views/snippets/snippets.xml index f8439986f65..a20bcf487a1 100644 --- a/addons/website_sale/views/snippets/snippets.xml +++ b/addons/website_sale/views/snippets/snippets.xml @@ -120,6 +120,13 @@ data-dependencies="attributes_opt, attributes_opt_top" data-no-preview="true" data-reload="/"/> + + title="" data-select-style="" data-css-property="background-color" data-color-prefix="text-bg-" data-apply-to=".o_ribbon"/> @@ -242,6 +249,11 @@ Options Products List + + @@ -340,7 +341,7 @@ data-bs-toggle="offcanvas" data-bs-target="#o_wsale_offcanvas"> - filters active + filters active @@ -649,6 +650,37 @@ + + + +
+

+ +

+
+
+
+ + + +
+
+
+
+
+ + + @@ -863,6 +900,34 @@ + + + + + + @@ -2371,11 +2477,16 @@