[IMP] website_sale: exclude the excluded variants from attribute search
Filter product templates search result if there are any possible attribute combination to get/create a product variant regarding the searched attributes. For example if the combination (A, B) is excluded, filtering on (A, B) will filter that product out of the result. task-2558970 closes odoo/odoo#89018 Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
committed by
William Braeckman
parent
c1aa010af7
commit
91d0e9c645
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from collections import defaultdict
|
||||
from itertools import product as cartesian_product
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
@@ -297,6 +299,53 @@ class WebsiteSale(http.Controller):
|
||||
limit=None, order=self._get_search_order(post), options=options)
|
||||
search_product = details[0].get('results', request.env['product.template']).with_context(bin_size=True)
|
||||
|
||||
if attrib_set:
|
||||
# Attributes value per attribute
|
||||
attribute_values = request.env['product.attribute.value'].browse(attrib_set)
|
||||
values_per_attribute = defaultdict(lambda: request.env['product.attribute.value'])
|
||||
# In case we have only one value per attribute we can check for a combination using those attributes directly
|
||||
multi_value_attribute = False
|
||||
for value in attribute_values:
|
||||
values_per_attribute[value.attribute_id] |= value
|
||||
if len(values_per_attribute[value.attribute_id]) > 1:
|
||||
multi_value_attribute = True
|
||||
|
||||
def filter_template(template, attribute_values_list):
|
||||
# Transform product.attribute.value to product.template.attribute.value
|
||||
attribute_value_to_ptav = dict()
|
||||
for ptav in template.attribute_line_ids.product_template_value_ids:
|
||||
attribute_value_to_ptav[ptav.product_attribute_value_id] = ptav.id
|
||||
possible_combinations = False
|
||||
for attribute_values in attribute_values_list:
|
||||
ptavs = request.env['product.template.attribute.value'].browse(
|
||||
[attribute_value_to_ptav[val] for val in attribute_values if val in attribute_value_to_ptav]
|
||||
)
|
||||
if len(ptavs) < len(attribute_values):
|
||||
# In this case the template is not compatible with this specific combination
|
||||
continue
|
||||
if len(ptavs) == len(template.attribute_line_ids):
|
||||
if template._is_combination_possible(ptavs):
|
||||
return True
|
||||
elif len(ptavs) < len(template.attribute_line_ids):
|
||||
if len(attribute_values_list) == 1:
|
||||
if any(template._get_possible_combinations(necessary_values=ptavs)):
|
||||
return True
|
||||
if not possible_combinations:
|
||||
possible_combinations = template._get_possible_combinations()
|
||||
if any(len(ptavs & combination) == len(ptavs) for combination in possible_combinations):
|
||||
return True
|
||||
return False
|
||||
|
||||
# If multi_value_attribute is False we know that we have our final combination (or at least a subset of it)
|
||||
if not multi_value_attribute:
|
||||
possible_attrib_values_list = [attribute_values]
|
||||
else:
|
||||
# Cartesian product from dict keys and values
|
||||
possible_attrib_values_list = [request.env['product.attribute.value'].browse([v.id for v in values]) for
|
||||
values in cartesian_product(*values_per_attribute.values())]
|
||||
|
||||
search_product = search_product.filtered(lambda tmpl: filter_template(tmpl, possible_attrib_values_list))
|
||||
|
||||
filter_by_price_enabled = website.is_view_active('website_sale.filter_products_price')
|
||||
if filter_by_price_enabled:
|
||||
# TODO Find an alternative way to obtain the domain through the search metadata.
|
||||
|
||||
@@ -204,6 +204,7 @@ const cartHandlerMixin = wSaleUtils.cartHandlerMixin;
|
||||
require("web.zoomodoo");
|
||||
const {extraMenuUpdateCallbacks} = require('website.content.menu');
|
||||
const dom = require('web.dom');
|
||||
const { cartesian } = require('@web/core/utils/arrays');
|
||||
|
||||
publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, cartHandlerMixin, {
|
||||
selector: '.oe_website_sale',
|
||||
@@ -773,23 +774,61 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, car
|
||||
$('.toggle_summary_div').toggleClass('d-none');
|
||||
$('.toggle_summary_div').removeClass('d-xl-block');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isValidCombination(combination, attributeExclusions) {
|
||||
if (attributeExclusions.exclusions) {
|
||||
for (const attribute of combination) {
|
||||
if (!attributeExclusions.exclusions.hasOwnProperty(attribute)) {
|
||||
continue;
|
||||
}
|
||||
for (const excludedAttribute of attributeExclusions.exclusions[attribute]) {
|
||||
if (combination.includes(excludedAttribute)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attributeExclusions.archived_combination) {
|
||||
for (const archivedCombination of attributeExclusions.archived_combination) {
|
||||
if (archivedCombination.length !== combination.length) {
|
||||
continue;
|
||||
}
|
||||
if (archivedCombination.filter((attr) => combination.includes(attr)).length === combination.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_applyHashFromSearch() {
|
||||
const params = $.deparam(window.location.search.slice(1));
|
||||
if (params.attrib) {
|
||||
const dataValueIds = [];
|
||||
for (const attrib of [].concat(params.attrib)) {
|
||||
const attribSplit = attrib.split('-');
|
||||
const attribValueSelector = `.js_variant_change[name="ptal-${attribSplit[0]}"][value="${attribSplit[1]}"]`;
|
||||
const attributeValuesPerAttribute = {};
|
||||
for (const attrib of params.attrib) {
|
||||
const [ptalId, ptavId] = attrib.split('-');
|
||||
const attribValueSelector = `.js_variant_change[name="ptal-${ptalId}"][value="${ptavId}"]`;
|
||||
const attribValue = this.el.querySelector(attribValueSelector);
|
||||
if (attribValue !== null) {
|
||||
dataValueIds.push(attribValue.dataset.value_id);
|
||||
if (!attributeValuesPerAttribute[ptalId]) {
|
||||
attributeValuesPerAttribute[ptalId] = [];
|
||||
}
|
||||
attributeValuesPerAttribute[ptalId].push(ptavId);
|
||||
}
|
||||
}
|
||||
if (dataValueIds.length) {
|
||||
window.location.hash = `attr=${dataValueIds.join(',')}`;
|
||||
const attributeSelection = this.el.querySelector('.js_add_cart_variants');
|
||||
const attributeExclusions = attributeSelection && JSON.parse(attributeSelection.dataset.attribute_exclusions);
|
||||
if (attributeExclusions && Object.values(attributeValuesPerAttribute).length > 1) {
|
||||
const allCombinations = cartesian(...Object.values(attributeValuesPerAttribute));
|
||||
const selectedCombination = allCombinations.find(c => this._isValidCombination(c, attributeExclusions));
|
||||
|
||||
if (selectedCombination && selectedCombination.length) {
|
||||
window.location.hash = `attr=${selectedCombination.join(',')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._applyHash();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('shop_variant_exclusion', {
|
||||
url: '/shop',
|
||||
test: true,
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "select product attribute First Attribute - Value 1",
|
||||
trigger: 'form.js_attributes input:not(:checked) + label:contains(First Attribute - Value 1)',
|
||||
},
|
||||
{
|
||||
content: "select product attribute Second Attribute - Value 2",
|
||||
trigger: 'form.js_attributes input:not(:checked) + label:contains(Second Attribute - Value 2)',
|
||||
},
|
||||
{
|
||||
content: "check for product template",
|
||||
trigger: '[data-oe-expression="product.name"]:contains(Test Product)',
|
||||
run: () => {}
|
||||
},
|
||||
{
|
||||
content: "deselect product attribute Second Attribute - Value 2",
|
||||
trigger: 'form.js_attributes input:checked + label:contains(Second Attribute - Value 2)',
|
||||
},
|
||||
{
|
||||
content: "select product attribute Second Attribute - Value 1",
|
||||
extra_trigger: 'body:not(:has(#customize-menu:visible .dropdown-menu:visible))',
|
||||
trigger: 'form.js_attributes input:not(:checked) + label:contains(Second Attribute - Value 1)',
|
||||
},
|
||||
{
|
||||
content: "check for no product defined",
|
||||
trigger: "h3:contains(No product defined)",
|
||||
run: () => {}
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -15,3 +15,4 @@ from . import test_website_sequence
|
||||
from . import test_website_sale_show_compare_list_price
|
||||
from . import test_website_sale_visitor
|
||||
from . import test_website_sale_product
|
||||
from . import test_website_sale_shop_variant_exclusion
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal
|
||||
from odoo.tests import tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestShopVariantExclusion(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestShopVariantExclusion, cls).setUpClass()
|
||||
# create a template
|
||||
cls.product_template = cls.env['product.template'].create({
|
||||
'name': 'Test Product',
|
||||
'is_published': True,
|
||||
'list_price': 750,
|
||||
})
|
||||
|
||||
cls.first_product_attribute = cls.env['product.attribute'].create({
|
||||
'name': 'First Attribute',
|
||||
'visibility': 'visible',
|
||||
'sequence': 10,
|
||||
})
|
||||
|
||||
cls.second_product_attribute = cls.env['product.attribute'].create({
|
||||
'name': 'Second Attribute',
|
||||
'visibility': 'visible',
|
||||
})
|
||||
|
||||
cls.product_attribute_value_1 = cls.env['product.attribute.value'].create({
|
||||
'name': 'First Attribute - Value 1',
|
||||
'attribute_id': cls.first_product_attribute.id,
|
||||
'sequence': 1,
|
||||
})
|
||||
cls.product_attribute_value_2 = cls.env['product.attribute.value'].create({
|
||||
'name': 'First Attribute - Value 2',
|
||||
'attribute_id': cls.first_product_attribute.id,
|
||||
})
|
||||
cls.product_attribute_value_3 = cls.env['product.attribute.value'].create({
|
||||
'name': 'Second Attribute - Value 1',
|
||||
'attribute_id': cls.second_product_attribute.id,
|
||||
})
|
||||
cls.product_attribute_value_4 = cls.env['product.attribute.value'].create({
|
||||
'name': 'Second Attribute - Value 2',
|
||||
'attribute_id': cls.second_product_attribute.id,
|
||||
})
|
||||
|
||||
# set attribute and attribute values on the template
|
||||
cls.env['product.template.attribute.line'].create([{
|
||||
'attribute_id': cls.first_product_attribute.id,
|
||||
'product_tmpl_id': cls.product_template.id,
|
||||
'value_ids': [(6, 0, [cls.product_attribute_value_1.id, cls.product_attribute_value_2.id])]
|
||||
}])
|
||||
|
||||
cls.env['product.template.attribute.line'].create([{
|
||||
'attribute_id': cls.second_product_attribute.id,
|
||||
'product_tmpl_id': cls.product_template.id,
|
||||
'value_ids': [(6, 0, [cls.product_attribute_value_3.id, cls.product_attribute_value_4.id])]
|
||||
}])
|
||||
|
||||
def _add_exclude(self, ptav1, ptav2, product_template):
|
||||
ptav1.update({
|
||||
'exclude_for': [(0, 0, {
|
||||
'product_tmpl_id': product_template.id,
|
||||
'value_ids': [(6, 0, [ptav2.id])]
|
||||
})]
|
||||
})
|
||||
|
||||
def _get_product_template_attribute_value(self, product_attribute_value, model):
|
||||
"""
|
||||
Return the `product.template.attribute.value` matching
|
||||
`product_attribute_value` for self.
|
||||
|
||||
:param: recordset of one product.attribute.value
|
||||
:return: recordset of one product.template.attribute.value if found
|
||||
else empty
|
||||
"""
|
||||
return model.valid_product_template_attribute_line_ids.filtered(
|
||||
lambda l: l.attribute_id == product_attribute_value.attribute_id
|
||||
).product_template_value_ids.filtered(
|
||||
lambda v: v.product_attribute_value_id == product_attribute_value
|
||||
)
|
||||
|
||||
def test_admin_shop_variant_exclusion(self):
|
||||
# Enable Variant Group
|
||||
self.env.ref('product.group_product_variant').write({'users': [(4, self.env.ref('base.user_admin').id)]})
|
||||
ptav1 = self._get_product_template_attribute_value(self.product_attribute_value_1, self.product_template)
|
||||
ptav2 = self._get_product_template_attribute_value(self.product_attribute_value_3, self.product_template)
|
||||
self._add_exclude(ptav1, ptav2, self.product_template)
|
||||
# Enable Product Attributes Left Panel
|
||||
self.env['ir.ui.view'].with_context(active_test=False).search(
|
||||
[('key', '=', 'website_sale.products_attributes')]).write({'active': True})
|
||||
self.start_tour("/", 'shop_variant_exclusion', login="admin")
|
||||
Reference in New Issue
Block a user