[IMP] product,website_sale(_product_configurator): allow extra-options

[A previous
commit](https://github.com/odoo/odoo/commit/5ca1da8ab33d4dd6a9ed0b6737e5d19f1a3ac9d2)
introduces a new attribute `display_type`, `multi`, which allows users
to define extra-options to products. But this option was only available
in Sales.

This commit allows eCommerce users to use this new attribute in the
online shop.

task-3497046

closes odoo/odoo#135174

Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Valentin Vallaeys (vava)
2023-09-22 11:55:53 +00:00
parent d191e69726
commit 30994723e9
6 changed files with 167 additions and 24 deletions
+25 -8
View File
@@ -953,12 +953,17 @@ class ProductTemplate(models.Model):
if ignore_no_variant:
attribute_lines = attribute_lines._without_no_variant_attributes()
if len(combination) != len(attribute_lines):
attribute_lines_without_multi = attribute_lines.filtered(
lambda l: l.attribute_id.display_type != 'multi')
combination_without_multi = combination.filtered(
lambda l: l.attribute_line_id.attribute_id.display_type != 'multi')
if len(combination_without_multi) != len(attribute_lines_without_multi):
# number of attribute values passed is different than the
# configuration of attributes on the template
return False
if attribute_lines != combination.attribute_line_id:
if attribute_lines_without_multi != combination_without_multi.attribute_line_id:
# combination has different attributes than the ones configured on the template
return False
@@ -1189,6 +1194,16 @@ class ProductTemplate(models.Model):
while True:
current_line_values = product_template_attribute_values_per_line[line_index]
current_ptav_index = value_index_per_line[line_index]
# For multi-checkbox attribute, the list is empty as we want to start without any selected value
if not current_line_values:
if line_index == len(product_template_attribute_values_per_line) - 1:
# submit combination if we're on the last line
yield partial_combination
else:
line_index += 1
continue
current_ptav = current_line_values[current_ptav_index]
# removing exclusions from current_ptav as we're removing it from partial_combination
@@ -1258,16 +1273,18 @@ class ProductTemplate(models.Model):
necessary_values = necessary_values or self.env['product.template.attribute.value']
necessary_attribute_lines = necessary_values.mapped('attribute_line_id')
attribute_lines = self.valid_product_template_attribute_line_ids.filtered(
lambda ptal: ptal not in necessary_attribute_lines and (
ptal.attribute_id.display_type != 'multi'))
lambda ptal: ptal not in necessary_attribute_lines)
if not attribute_lines and self._is_combination_possible(necessary_values, parent_combination):
yield necessary_values
product_template_attribute_values_per_line = [
ptal.product_template_value_ids._only_active()
for ptal in attribute_lines
]
product_template_attribute_values_per_line = []
for ptal in attribute_lines:
if ptal.attribute_id.display_type != 'multi':
values_to_add = ptal.product_template_value_ids._only_active()
else:
values_to_add = self.env['product.template.attribute.value']
product_template_attribute_values_per_line.append(values_to_add)
for partial_combination in self._cartesian_product(product_template_attribute_values_per_line, parent_combination):
combination = partial_combination + necessary_values
@@ -0,0 +1,60 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import tourUtils from "@website_sale/js/tours/tour_utils";
// This tour relies on a data created from the python test.
registry.category("web_tour.tours").add('tour_shop_multi_checkbox', {
test: true,
url: '/shop?search=Product Multi',
steps: () => [
{
content: "select Product",
trigger: '.oe_product_cart a:containsExact("Product Multi")',
},
{
content: "check price",
trigger: '.oe_currency_value:contains("750")',
run: function () {},
},
{
content: 'click on the first option to select it',
trigger: 'input[data-attribute_name="Options"][data-value_name="Option 1"]',
},
{
content: 'click on the third option to select it',
trigger: 'input[data-attribute_name="Options"][data-value_name="Option 3"]',
},
{
content: 'check combination is not possible',
trigger: '.js_main_product.css_not_available .css_not_available_msg:contains("This combination does not exist.")'
},
{
content: "check add to cart not possible",
trigger: '#add_to_cart.disabled',
run: function () {},
},
{
content: 'click on the third option to unselect it',
trigger: 'input[data-attribute_name="Options"][data-value_name="Option 3"]',
},
{
content: 'click on the second option to select it',
trigger: 'input[data-attribute_name="Options"][data-value_name="Option 2"]',
},
{
content: "check price of options is correct",
trigger: '.oe_currency_value:contains("753")',
run: function () {},
},
{
content: "add to cart",
trigger: 'a:contains(ADD TO CART)',
},
tourUtils.goToCart(),
{
content: "check price is correct",
trigger: '.td-product_name:contains("Options: Option 1, Option 2")',
run: function () {},
},
]});
@@ -445,3 +445,60 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
])
self.start_tour("/", 'test_09_pills_variant', login="portal")
def test_10_multi_checkbox_attribute(self):
product_template = self.env['product.template'].create({
'name': 'Product Multi',
'is_published': True,
'list_price': 750,
})
attribute = self.env['product.attribute'].create([
{
'name': 'Options',
'create_variant': 'no_variant',
'display_type': 'multi',
},
])
attribute_values = self.env['product.attribute.value'].create([
{
'name': 'Option 1',
'attribute_id': attribute.id,
'default_extra_price': 1,
'sequence': 1,
},
{
'name': 'Option 2',
'attribute_id': attribute.id,
'sequence': 2,
},
{
'name': 'Option 3',
'attribute_id': attribute.id,
'default_extra_price': 3,
'sequence': 3,
},
{
'name': 'Option 4',
'attribute_id': attribute.id,
'sequence': 4,
},
])
self.env['product.template.attribute.line'].create([{
'attribute_id': attribute.id,
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, attribute_values.ids)],
}])
# set an extra price for free attribute values on the product (nothing is free)
self.env['product.template.attribute.value'].search(
[('product_tmpl_id', '=', product_template.id), ('price_extra', '=', 0)]
).price_extra = 2
# set an exclusion between option 1 and option 3
self.env['product.template.attribute.value'].search(
[('product_tmpl_id', '=', product_template.id), ('price_extra', '=', 1)]
).exclude_for = [(0, 0, {
'product_tmpl_id': product_template.id,
'value_ids': [(6, 0, [self.env['product.template.attribute.value'].search(
[('product_tmpl_id', '=', product_template.id), ('price_extra', '=', 3)]).id])]
})]
self.start_tour("/", 'tour_shop_multi_checkbox', login="portal")
+2 -5
View File
@@ -859,7 +859,7 @@
</t>
</select>
</t>
<t t-if="a.display_type == 'radio' or a.display_type == 'pills'">
<t t-if="a.display_type in ('radio', 'pills', 'multi')">
<div class="flex-column mb-3">
<t t-foreach="a.value_ids" t-as="v">
<div class="form-check mb-1">
@@ -1033,13 +1033,10 @@
<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]" />
<t t-if="combination" t-call="website_sale.variants">
<t t-call="website_sale.variants">
<t t-set="ul_class" t-valuef="flex-column" />
<t t-set="parent_combination" t-value="None" />
</t>
<t t-else="">
<ul class="d-none js_add_cart_variants mb-0" t-att-data-attribute_exclusions="{'exclusions: []'}"/>
</t>
</t>
<p t-if="True" class="css_not_available_msg alert alert-warning">This combination does not exist.</p>
<div id="o_wsale_cta_wrapper" class="d-flex flex-wrap align-items-center">
@@ -36,13 +36,13 @@
</select>
</t>
<t t-if="ptal.attribute_id.display_type == 'radio'">
<t t-elif="ptal.attribute_id.display_type in ('radio', 'multi')">
<ul t-att-data-attribute_id="ptal.attribute_id.id" t-attf-class="list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}">
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav">
<li class="list-inline-item mb-3 js_attribute_value" style="margin: 0;">
<label class="col-form-label">
<div class="form-check">
<input type="radio"
<input t-att-type="'radio' if ptal.attribute_id.display_type == 'radio' else 'checkbox'"
t-attf-class="form-check-input js_variant_change #{ptal.attribute_id.create_variant}"
t-att-checked="ptav in combination"
t-att-name="'ptal-%s' % ptal.id"
@@ -52,7 +52,7 @@
t-att-data-attribute_name="ptav.attribute_id.name"
t-att-data-is_custom="ptav.is_custom"
t-att-data-is_single="single"
t-att-data-is_single_and_custom="single_and_custom" />
t-att-data-is_single_and_custom="single_and_custom"/>
<div class="radio_input_value form-check-label">
<span t-field="ptav.name"/>
<t t-call="website_sale.badge_extra_price"/>
@@ -64,7 +64,7 @@
</ul>
</t>
<t t-if="ptal.attribute_id.display_type == 'pills'">
<t t-elif="ptal.attribute_id.display_type == 'pills'">
<ul t-att-data-attribute_id="ptal.attribute_id.id"
t-attf-class="btn-group-toggle list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}"
data-bs-toggle="buttons">
@@ -92,7 +92,7 @@
</ul>
</t>
<t t-if="ptal.attribute_id.display_type == 'color'">
<t t-elif="ptal.attribute_id.display_type == 'color'">
<ul t-att-data-attribute_id="ptal.attribute_id.id" t-attf-class="list-inline o_wsale_product_attribute #{'d-none' if single_and_custom else ''}">
<li t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav" class="list-inline-item me-1">
<label t-attf-style="background-color:#{ptav.html_color or ptav.product_attribute_value_id.name if not ptav.is_custom else ''}"
@@ -257,9 +257,15 @@ export const OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin,
$.each(this.rootProduct.no_variant_attribute_values, function () {
if (this.is_custom !== 'True') {
$updatedDescription.append($('<div>', {
text: this.attribute_name + ': ' + this.attribute_value_name
}));
var $currentDescription = $updatedDescription.find(`div[name=ptal-${this.id}]`);
if ($currentDescription?.length > 0) { // one row per multicheckbox
$currentDescription.text($currentDescription.text() + ', ' + this.attribute_value_name);
} else {
$updatedDescription.append($('<div>', {
text: this.attribute_name + ': ' + this.attribute_value_name,
name: `ptal-${this.id}`,
}));
}
}
});
@@ -356,9 +362,15 @@ export const OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin,
$.each(noVariantAttributeValues, function (){
if (this.is_custom !== 'True'){
$customAttributeValuesDescription.append($('<div>', {
text: this.attribute_name + ': ' + this.attribute_value_name
}));
var $currentDescription = $customAttributeValuesDescription.find(`div[name=ptal-${this.id}]`);
if ($currentDescription?.length > 0) { // one row per multicheckbox
$currentDescription.text($currentDescription.text() + ', ' + this.attribute_value_name);
} else {
$customAttributeValuesDescription.append($('<div>', {
text: this.attribute_name + ': ' + this.attribute_value_name,
name: `ptal-${this.id}`,
}));
}
}
});