', { + text: $productDescription.text() + })); + $.each(this.rootProduct.product_custom_attribute_values, function () { + if (this.custom_value) { + const $customInput = $modalContent + .find(".main_product [data-is_custom='True']") + .closest(`[data-value_id='${this.custom_product_template_attribute_value_id.res_id}']`); + $customInput.attr('previous_custom_value', this.custom_value); + VariantMixin.handleCustomValues($customInput); + } + }); + + $.each(this.rootProduct.no_variant_attribute_values, function () { + if (this.is_custom !== 'True') { + 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($('
').text('image variant option src changed').insertAfter('.oe_advanced_configurator_modal .js_product:eq(1) .product-name');
+ }
+ }
+}, {
+ extra_trigger: '.oe_advanced_configurator_modal .js_product:eq(1) div:contains("image variant option src changed")',
+ trigger: '.oe_advanced_configurator_modal .js_product:eq(1) input[data-value_name="Steel"]',
+}, {
+ trigger: '.oe_price span:contains(22.90)',
+ run: function (){}, // check
+}, {
+ trigger: '.oe_advanced_configurator_modal .js_product:has(strong:contains(Conference Chair)) .js_add',
+ extra_trigger: '.oe_advanced_configurator_modal .js_product:has(strong:contains(Conference Chair))',
+ run: 'click'
+}, {
+ trigger: '.oe_advanced_configurator_modal .js_product:has(strong:contains(Chair floor protection)) .js_add',
+ extra_trigger: '.oe_advanced_configurator_modal .js_product:has(strong:contains(Chair floor protection))',
+ run: 'click'
+}, {
+ trigger: 'span:contains(1,557.00)',
+ run: function (){}, // check
+}, {
+ trigger: 'button:has(span:contains(Proceed to Checkout))',
+ run: 'click',
+}]});
diff --git a/static/tests/tours/website_sale_variants_modal_window.js b/static/tests/tours/website_sale_variants_modal_window.js
new file mode 100644
index 0000000..2e0ab10
--- /dev/null
+++ b/static/tests/tours/website_sale_variants_modal_window.js
@@ -0,0 +1,65 @@
+/** @odoo-module **/
+
+ import { registry } from "@web/core/registry";
+
+ // This tour relies on a data created from the python test.
+ registry.category("web_tour.tours").add('tour_variants_modal_window', {
+ test: true,
+ url: '/shop?search=Short (TEST)',
+ steps: () => [
+ {
+ content: "Select the Short (TEST) product",
+ trigger: '.oe_product_cart a:containsExact("Short (TEST)")',
+ },
+ {
+ content: "Click on the always variant",
+ trigger: 'input[data-attribute_name="Always attribute size"][data-value_name="M always"]',
+ },
+ {
+ content: "Click on the dynamic variant",
+ trigger: 'input[data-attribute_name="Dynamic attribute size"][data-value_name="M dynamic"]',
+ },
+ {
+ content: "Click on the never variant",
+ trigger: 'input[data-attribute_name="Never attribute size"][data-value_name="M never"]',
+ },
+ {
+ content: "Click on the never custom variant",
+ trigger: 'input[data-attribute_name="Never attribute size custom"][data-value_name="Yes never custom"]',
+ },
+ {
+ trigger: 'input.variant_custom_value',
+ run: 'text TEST',
+ },
+ {
+ content: "Click add to cart",
+ trigger: '#add_to_cart',
+ },
+ {
+ content: "Go through the modal window of the product configurator",
+ extra_trigger: '.oe_advanced_configurator_modal',
+ trigger: 'button span:contains(Proceed to Checkout)',
+ run: 'click'
+ },
+ {
+ content: "Check the product is in the cart",
+ trigger: 'div>a>h6:contains(Short (TEST))',
+ },
+ {
+ content: "Check always variant",
+ trigger: 'div>a>h6:contains(M always)',
+ },
+ {
+ content: "Check dynamic variant",
+ trigger: 'div>a>h6:contains(M dynamic)',
+ },
+ {
+ content: "Check never variant",
+ trigger: 'div.text-muted>span:contains(Never attribute size: M never)',
+ },
+ {
+ content: "Check never custom variant",
+ trigger: 'div.text-muted>span:contains(Never attribute size custom: Yes never custom: TEST)',
+ isCheck: true,
+ }
+ ]});
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..a89d4e7
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_customize
+from . import test_website_sale_configurator
diff --git a/tests/test_customize.py b/tests/test_customize.py
new file mode 100644
index 0000000..b97b078
--- /dev/null
+++ b/tests/test_customize.py
@@ -0,0 +1,15 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import HttpCase
+from odoo.addons.sale_product_configurator.tests.common import TestProductConfiguratorCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestUi(HttpCase, TestProductConfiguratorCommon):
+
+ def test_01_admin_shop_custom_attribute_value_tour(self):
+ # Ensure that no pricelist is available during the test.
+ # This ensures that tours which triggers on the amounts will run properly.
+ self.env['product.pricelist'].search([]).action_archive()
+ self.start_tour("/", 'a_shop_custom_attribute_value', login="admin")
diff --git a/tests/test_website_sale_configurator.py b/tests/test_website_sale_configurator.py
new file mode 100644
index 0000000..65666e4
--- /dev/null
+++ b/tests/test_website_sale_configurator.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import tagged
+from odoo.addons.sale_product_configurator.tests.common import TestProductConfiguratorCommon
+from odoo.addons.base.tests.common import HttpCaseWithUserPortal, HttpCaseWithUserDemo
+
+
+@tagged('post_install', '-at_install')
+class TestWebsiteSaleProductConfigurator(TestProductConfiguratorCommon, HttpCaseWithUserPortal, HttpCaseWithUserDemo):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.product_product_custo_desk.write({
+ 'optional_product_ids': [(4, cls.product_product_conf_chair.id)],
+ 'website_published': True,
+ })
+ cls.product_product_conf_chair.website_published = True
+
+ ptav_ids = cls.product_product_custo_desk.attribute_line_ids.product_template_value_ids
+ ptav_ids.filtered(lambda ptav: ptav.name == 'Aluminium').price_extra = 50.4
+
+ def test_01_product_configurator_variant_price(self):
+ product = self.product_product_conf_chair.with_user(self.user_portal)
+ ptav_ids = self.product_product_custo_desk.attribute_line_ids.product_template_value_ids
+ parent_combination = ptav_ids.filtered(lambda ptav: ptav.name in ('Aluminium', 'White'))
+ self.assertEqual(product._is_add_to_cart_possible(parent_combination), True)
+ # This is a regression test. The product configurator menu is proposed
+ # whenever a product has optional products. However, as the end user
+ # already picked a variant, the variant configuration menu is omitted
+ # in this case. However, we still want to make sure that the correct
+ # variant attributes are taken into account when calculating the price.
+ url = self.product_product_custo_desk.website_url
+ # Ensure that no pricelist is available during the test.
+ # This ensures that tours with triggers on the amounts will run properly.
+ self.env['product.pricelist'].search([]).action_archive()
+ self.start_tour(url, 'website_sale_product_configurator_optional_products_tour', login='portal')
+
+ def test_02_variants_modal_window(self):
+ """
+ The objective is to verify that the data concerning the variants are well transmitted
+ even when passing through a modal window (product configurator).
+
+ We create a product with the different attributes and we will modify them.
+ If the information is not correctly transmitted,
+ the default values of the variants will be used (the first one).
+ """
+
+ always_attribute, dynamic_attribute, never_attribute, never_attribute_custom = self.env['product.attribute'].create([
+ {
+ 'name': 'Always attribute size',
+ 'display_type': 'radio',
+ 'create_variant': 'always'
+ },
+ {
+ 'name': 'Dynamic attribute size',
+ 'display_type': 'radio',
+ 'create_variant': 'dynamic'
+ },
+ {
+ 'name': 'Never attribute size',
+ 'display_type': 'radio',
+ 'create_variant': 'no_variant'
+ },
+ {
+ 'name': 'Never attribute size custom',
+ 'display_type': 'radio',
+ 'create_variant': 'no_variant'
+ }
+ ])
+ always_S, always_M, dynamic_S, dynamic_M, never_S, never_M, never_custom_no, never_custom_yes = self.env['product.attribute.value'].create([
+ {
+ 'name': 'S always',
+ 'attribute_id': always_attribute.id,
+ },
+ {
+ 'name': 'M always',
+ 'attribute_id': always_attribute.id,
+ },
+ {
+ 'name': 'S dynamic',
+ 'attribute_id': dynamic_attribute.id,
+ },
+ {
+ 'name': 'M dynamic',
+ 'attribute_id': dynamic_attribute.id,
+ },
+ {
+ 'name': 'S never',
+ 'attribute_id': never_attribute.id,
+ },
+ {
+ 'name': 'M never',
+ 'attribute_id': never_attribute.id,
+ },
+ {
+ 'name': 'No never custom',
+ 'attribute_id': never_attribute_custom.id,
+ },
+ {
+ 'name': 'Yes never custom',
+ 'attribute_id': never_attribute_custom.id,
+ 'is_custom': True,
+ }
+ ])
+
+ product_short = self.env['product.template'].create({
+ 'name': 'Short (TEST)',
+ 'website_published': True,
+ })
+
+ self.env['product.template.attribute.line'].create([
+ {
+ 'product_tmpl_id': product_short.id,
+ 'attribute_id': always_attribute.id,
+ 'value_ids': [(4, always_S.id), (4, always_M.id)],
+ },
+ {
+ 'product_tmpl_id': product_short.id,
+ 'attribute_id': dynamic_attribute.id,
+ 'value_ids': [(4, dynamic_S.id), (4, dynamic_M.id)],
+ },
+ {
+ 'product_tmpl_id': product_short.id,
+ 'attribute_id': never_attribute.id,
+ 'value_ids': [(4, never_S.id), (4, never_M.id)],
+ },
+ {
+ 'product_tmpl_id': product_short.id,
+ 'attribute_id': never_attribute_custom.id,
+ 'value_ids': [(4, never_custom_no.id), (4, never_custom_yes.id)],
+ },
+ ])
+
+ # Add an optional product to trigger the modal window
+ optional_product = self.env['product.template'].create({
+ 'name': 'Optional product (TEST)',
+ 'website_published': True,
+ })
+ product_short.optional_product_ids = [(4, optional_product.id)]
+
+ old_sale_order = self.env['sale.order'].search([])
+ self.start_tour("/", 'tour_variants_modal_window', login="demo")
+
+ # Check the name of the created sale order line
+ new_sale_order = self.env['sale.order'].search([]) - old_sale_order
+ new_order_line = new_sale_order.order_line
+ self.assertEqual(new_order_line.name, 'Short (TEST) (M always, M dynamic)\n\nNever attribute size: M never\nNever attribute size custom: Yes never custom: TEST')
diff --git a/views/templates.xml b/views/templates.xml
new file mode 100644
index 0000000..775ed93
--- /dev/null
+++ b/views/templates.xml
@@ -0,0 +1,172 @@
+
+ Option not available Option not available
+
+
+
+
+
+
+
+
+
+
+
+ Product
+
+
+
+
+
+
+ Quantity
+
+
+
+ Price
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total:
+
+
+
+ Available Options:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+