[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:
@@ -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")
|
||||
|
||||
@@ -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 ''}"
|
||||
|
||||
+18
-6
@@ -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}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user