diff --git a/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml b/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml
new file mode 100644
index 00000000000..fb863754ea0
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
index 0036d50b625..fdd78d7b634 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/Orderline.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js
new file mode 100644
index 00000000000..7afedbf6091
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/ProductConfigurator.tour.js
@@ -0,0 +1,66 @@
+odoo.define('point_of_sale.tour.ProductConfigurator', function (require) {
+ 'use strict';
+
+ const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
+ const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods');
+ const { getSteps, startSteps } = require('point_of_sale.tour.utils');
+ var Tour = require('web_tour.tour');
+
+ // signal to start generating steps
+ // when finished, steps can be taken from getSteps
+ startSteps();
+
+ // Go by default to home category
+ ProductScreen.do.clickHomeCategory();
+
+ // Click on Configurable Chair product
+ ProductScreen.do.clickDisplayedProduct('Configurable Chair');
+ ProductConfigurator.check.isShown();
+
+ // Cancel configuration, not product should be in order
+ ProductConfigurator.do.cancelAttributes();
+ ProductScreen.check.orderIsEmpty();
+
+ // Click on Configurable Chair product
+ ProductScreen.do.clickDisplayedProduct('Configurable Chair');
+ ProductConfigurator.check.isShown();
+
+ // Pick Color
+ ProductConfigurator.do.pickColor('Red');
+
+ // Pick Radio
+ ProductConfigurator.do.pickSelect('Metal');
+
+ // Pick Select
+ ProductConfigurator.do.pickRadio('Other');
+
+ // Fill in custom attribute
+ ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
+
+ // Confirm configuration
+ ProductConfigurator.do.confirmAttributes();
+
+ // Check that the product has been added to the order with correct attributes and price
+ ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0');
+
+ // Orderlines with the same attributes should be merged
+ ProductScreen.do.clickHomeCategory();
+ ProductScreen.do.clickDisplayedProduct('Configurable Chair');
+ ProductConfigurator.do.pickColor('Red');
+ ProductConfigurator.do.pickSelect('Metal');
+ ProductConfigurator.do.pickRadio('Other');
+ ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
+ ProductConfigurator.do.confirmAttributes();
+ ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0');
+
+ // Orderlines with different attributes shouldn't be merged
+ ProductScreen.do.clickHomeCategory();
+ ProductScreen.do.clickDisplayedProduct('Configurable Chair');
+ ProductConfigurator.do.pickColor('Blue');
+ ProductConfigurator.do.pickSelect('Metal');
+ ProductConfigurator.do.pickRadio('Leather');
+ ProductConfigurator.do.confirmAttributes();
+ ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0');
+
+ Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/web' }, getSteps());
+});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js
new file mode 100644
index 00000000000..5d10f9fd430
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js
@@ -0,0 +1,77 @@
+odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) {
+ 'use strict';
+
+ const { createTourMethods } = require('point_of_sale.tour.utils');
+
+ class Do {
+ pickRadio(name) {
+ return [
+ {
+ content: `picking radio attribute with name ${name}`,
+ trigger: `.product-configurator-popup .radio_attribute_label:contains('${name}')`,
+ },
+ ];
+ }
+
+ pickSelect(name) {
+ return [
+ {
+ content: `picking select attribute with name ${name}`,
+ trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`,
+ run: `text ${name}`,
+ },
+ ];
+ }
+
+ pickColor(name) {
+ return [
+ {
+ content: `picking color attribute with name ${name}`,
+ trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`,
+ },
+ ];
+ }
+
+ fillCustomAttribute(value) {
+ return [
+ {
+ content: `filling custom attribute with value ${value}`,
+ trigger: `.product-configurator-popup .custom_value`,
+ run: `text ${value}`,
+ },
+ ];
+ }
+
+ confirmAttributes() {
+ return [
+ {
+ content: `confirming product configuration`,
+ trigger: `.product-configurator-popup .button.confirm`,
+ },
+ ];
+ }
+
+ cancelAttributes() {
+ return [
+ {
+ content: `canceling product configuration`,
+ trigger: `.product-configurator-popup .button.cancel`,
+ },
+ ];
+ }
+ }
+
+ class Check {
+ isShown() {
+ return [
+ {
+ content: 'product configurator is shown',
+ trigger: '.product-configurator-popup:not(:has(.oe_hidden))',
+ run: () => {},
+ },
+ ];
+ }
+ }
+
+ return createTourMethods('ProductConfigurator', Do, Check);
+});
diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py
index 759c23bee1e..73e9142de86 100644
--- a/addons/point_of_sale/tests/test_frontend.py
+++ b/addons/point_of_sale/tests/test_frontend.py
@@ -109,6 +109,12 @@ class TestPointOfSaleHttpCommon(odoo.tests.HttpCase):
'list_price': 5.10,
'taxes_id': False,
})
+ configurable_chair = env['product.product'].create({
+ 'name': 'Configurable Chair',
+ 'available_in_pos': True,
+ 'list_price': 10,
+ 'taxes_id': False,
+ })
attribute = env['product.attribute'].create({
'name': 'add 2',
@@ -124,6 +130,68 @@ class TestPointOfSaleHttpCommon(odoo.tests.HttpCase):
})
line.product_template_value_ids[0].price_extra = 2
+ chair_color_attribute = env['product.attribute'].create({
+ 'name': 'Color',
+ 'display_type': 'color',
+ 'create_variant': 'no_variant',
+ })
+ chair_color_red = env['product.attribute.value'].create({
+ 'name': 'Red',
+ 'attribute_id': chair_color_attribute.id,
+ 'html_color': '#ff0000',
+ })
+ chair_color_blue = env['product.attribute.value'].create({
+ 'name': 'Blue',
+ 'attribute_id': chair_color_attribute.id,
+ 'html_color': '#0000ff',
+ })
+ chair_color_line = env['product.template.attribute.line'].create({
+ 'product_tmpl_id': configurable_chair.product_tmpl_id.id,
+ 'attribute_id': chair_color_attribute.id,
+ 'value_ids': [(6, 0, [chair_color_red.id, chair_color_blue.id])]
+ })
+ chair_color_line.product_template_value_ids[0].price_extra = 1
+
+ chair_legs_attribute = env['product.attribute'].create({
+ 'name': 'Chair Legs',
+ 'display_type': 'select',
+ 'create_variant': 'no_variant',
+ })
+ chair_legs_metal = env['product.attribute.value'].create({
+ 'name': 'Metal',
+ 'attribute_id': chair_legs_attribute.id,
+ })
+ chair_legs_wood = env['product.attribute.value'].create({
+ 'name': 'Wood',
+ 'attribute_id': chair_legs_attribute.id,
+ })
+ chair_legs_line = env['product.template.attribute.line'].create({
+ 'product_tmpl_id': configurable_chair.product_tmpl_id.id,
+ 'attribute_id': chair_legs_attribute.id,
+ 'value_ids': [(6, 0, [chair_legs_metal.id, chair_legs_wood.id])]
+ })
+
+ chair_fabrics_attribute = env['product.attribute'].create({
+ 'name': 'Fabrics',
+ 'display_type': 'radio',
+ 'create_variant': 'no_variant',
+ })
+ chair_fabrics_leather = env['product.attribute.value'].create({
+ 'name': 'Leather',
+ 'attribute_id': chair_fabrics_attribute.id,
+ })
+ chair_fabrics_other = env['product.attribute.value'].create({
+ 'name': 'Other',
+ 'attribute_id': chair_fabrics_attribute.id,
+ 'is_custom': True,
+ })
+ chair_fabrics_line = env['product.template.attribute.line'].create({
+ 'product_tmpl_id': configurable_chair.product_tmpl_id.id,
+ 'attribute_id': chair_fabrics_attribute.id,
+ 'value_ids': [(6, 0, [chair_fabrics_leather.id, chair_fabrics_other.id])]
+ })
+ chair_color_line.product_template_value_ids[1].is_custom = True
+
fixed_pricelist = env['product.pricelist'].create({
'name': 'Fixed',
'item_ids': [(0, 0, {
@@ -437,3 +505,8 @@ class TestUi(TestPointOfSaleHttpCommon):
self.main_pos_config.write({ 'manage_orders': True, 'module_account': True })
self.main_pos_config.open_session_cb(check_coa=False)
self.start_tour("/pos/web?config_id=%d" % self.main_pos_config.id, 'OrderManagementScreenTour', login="admin", step_delay=50)
+
+ def test_04_product_configurator(self):
+ self.main_pos_config.write({ 'product_configurator': True })
+ self.main_pos_config.open_session_cb(check_coa=False)
+ self.start_tour("/pos/web?config_id=%d" % self.main_pos_config, 'ProductConfiguratorTour', login="admin", step_delay=50)
diff --git a/addons/point_of_sale/views/pos_assets_common.xml b/addons/point_of_sale/views/pos_assets_common.xml
index 6979503c517..1c191806a40 100644
--- a/addons/point_of_sale/views/pos_assets_common.xml
+++ b/addons/point_of_sale/views/pos_assets_common.xml
@@ -93,6 +93,7 @@
+
diff --git a/addons/point_of_sale/views/pos_assets_index.xml b/addons/point_of_sale/views/pos_assets_index.xml
index 90d35211bd2..65799f3c027 100644
--- a/addons/point_of_sale/views/pos_assets_index.xml
+++ b/addons/point_of_sale/views/pos_assets_index.xml
@@ -7,6 +7,7 @@
+
@@ -18,6 +19,7 @@
+
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml
index 0400e6e4e91..df1a7ed9423 100644
--- a/addons/point_of_sale/views/pos_config_view.xml
+++ b/addons/point_of_sale/views/pos_config_view.xml
@@ -96,6 +96,17 @@
+