From d7a0b59068eb6ddf19721465070137835b5b79cd Mon Sep 17 00:00:00 2001 From: fja-odoo Date: Fri, 31 May 2019 13:32:37 +0000 Subject: [PATCH] [IMP] website_sale, *: allow changing qty of optional products * sale, sale_management, sale_product_configurator, website_sale_product_configurator 1. Product Configurator ----------------------- - It is now possible to change the quantity of an optional product - Default optional product quantity is set to its parent product current quantity. - The order in which optional products of an option appear when added to the cart is fixed it will now be displayed below the correct product even if there are multiple options with the same product template id. To achieve this a unique id has been added to the products and a reference to their parent unique id. - The way products are given to the backend when configuration is done has been improved, now only an array with the product and useful info is submitted. 2. eCommerce in cart review --------------------------- - It is now possible to change the quantity of an accessory product. - It is now possible to delete an optional product added through the product configurator. - The labels "Option: optional product name" below the parent product are now removed when a related optional product is deleted. - The labels "Option: optional product name" are now displayed below the correct product. task-1951560 closes odoo/odoo#33002 Signed-off-by: Quentin Smetz (qsm) --- addons/sale/static/src/js/variant_mixin.js | 27 +-- .../static/src/scss/product_configurator.scss | 4 - .../views/sale_portal_templates.xml | 2 +- .../src/js/product_configurator_modal.js | 221 +++++++++--------- ...oduct_configurator_optional_products_ui.js | 46 +++- .../test_sale_product_configurator_ui.py | 7 +- .../views/templates.xml | 21 +- addons/website_sale/models/sale_order.py | 39 ++-- .../static/src/js/website_sale.js | 2 +- addons/website_sale/views/templates.xml | 7 +- .../controllers/main.py | 86 +++---- .../static/src/js/website_sale_options.js | 28 +-- .../static/src/scss/website_sale_options.scss | 4 - 13 files changed, 250 insertions(+), 244 deletions(-) diff --git a/addons/sale/static/src/js/variant_mixin.js b/addons/sale/static/src/js/variant_mixin.js index 2fc61923fb8..b03d3c623c8 100644 --- a/addons/sale/static/src/js/variant_mixin.js +++ b/addons/sale/static/src/js/variant_mixin.js @@ -33,49 +33,32 @@ var VariantMixin = { * are not relevant * * @param {MouseEvent} ev - * @param {$.Element} [params.$container] force the used container */ - onChangeVariant: function (ev, params) { + onChangeVariant: function (ev) { var $parent = $(ev.target).closest('.js_product'); if (!$parent.data('uniqueId')) { $parent.data('uniqueId', _.uniqueId()); } - this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev, params); + this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev); }, /** * @see onChangeVariant * * @private * @param {Event} ev - * @param {$.Element} [params.$container] force the used container * @returns {Deferred} */ - _getCombinationInfo: function (ev, params) { + _getCombinationInfo: function (ev) { var self = this; if ($(ev.target).hasClass('variant_custom_value')) { return Promise.resolve(); } - var $component; - if (params && params.$container) { - $component = params.$container; - } else if ($(ev.currentTarget).closest('form').length > 0){ - $component = $(ev.currentTarget).closest('form'); - } else if ($(ev.currentTarget).closest('.oe_optional_products_modal').length > 0){ - $component = $(ev.currentTarget).closest('.oe_optional_products_modal'); - } else if ($(ev.currentTarget).closest('.o_product_configurator').length > 0) { - $component = $(ev.currentTarget).closest('.o_product_configurator'); - } else { - $component = $(ev.currentTarget); - } - var qty = $component.find('input[name="add_qty"]').val(); - var $parent = $(ev.target).closest('.js_product'); - + var qty = $parent.find('input[name="add_qty"]').val(); var combination = this.getSelectedVariantValues($parent); var parentCombination = $parent.find('ul[data-attribute_exclusions]').data('attribute_exclusions').parent_combination; - var productTemplateId = parseInt($parent.find('.product_template_id').val()); self._checkExclusions($parent, combination); @@ -195,7 +178,7 @@ var VariantMixin = { */ triggerVariantChange: function ($container) { var self = this; - $container.find('ul[data-attribute_exclusions]').trigger('change', {$container: $container}); + $container.find('ul[data-attribute_exclusions]').trigger('change'); $container.find('input.js_variant_change:checked, select.js_variant_change').each(function () { self.handleCustomValues($(this)); }); diff --git a/addons/sale/static/src/scss/product_configurator.scss b/addons/sale/static/src/scss/product_configurator.scss index 3ab0daf9f2f..fde384b8f70 100644 --- a/addons/sale/static/src/scss/product_configurator.scss +++ b/addons/sale/static/src/scss/product_configurator.scss @@ -27,10 +27,6 @@ } .css_not_available.js_product { - > *:nth-child(4) > * { - display: none; - } - .css_quantity, .product_price { display: none; diff --git a/addons/sale_management/views/sale_portal_templates.xml b/addons/sale_management/views/sale_portal_templates.xml index 1400fd7910f..1f7fbcccf71 100644 --- a/addons/sale_management/views/sale_portal_templates.xml +++ b/addons/sale_management/views/sale_portal_templates.xml @@ -79,7 +79,7 @@ -
+
diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js index c2687e79d0d..5f57e0b3d44 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js @@ -6,13 +6,11 @@ var Dialog = require('web.Dialog'); var ServicesMixin = require('web.ServicesMixin'); var VariantMixin = require('sale.VariantMixin'); -var optionalProductsMap = {}; - var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { events: _.extend({}, Dialog.prototype.events, VariantMixin.events, { 'click a.js_add, a.js_remove': '_onAddOrRemoveOption', 'click button.js_add_cart_json': 'onClickAddCartJSON', - 'change .in_cart.main_product input.js_quantity': '_onChangeQuantity', + 'change .in_cart input.js_quantity': '_onChangeQuantity', 'change .js_raw_price': '_computePriceTotal' }), /** @@ -61,8 +59,6 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { this.previousModalHeight = params.previousModalHeight; this.dialogClass = 'oe_optional_products_modal'; this._productImageField = 'image_128'; - // reset any previously populated properties maps - optionalProductsMap = {}; this._opened.then(function () { if (self.previousModalHeight) { @@ -89,7 +85,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { } }) .then(function (modalContent) { - if (modalContent){ + if (modalContent) { var $modalContent = $(modalContent); $modalContent = self._postProcessContent($modalContent); self.$content = $modalContent; @@ -115,7 +111,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { var self = this; this.appendTo($('
')).then(function () { - if (!self.preventOpening){ + if (!self.preventOpening) { self.$modal.find(".modal-body").replaceWith(self.$el); self.$modal.attr('open', true); self.$modal.removeAttr("aria-hidden"); @@ -141,6 +137,20 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { this.$el.find('input[name="add_qty"]').val(this.rootProduct.quantity); + // set a unique id to each row for options hierarchy + var $products = this.$el.find('tr.js_product'); + _.each($products, function (el) { + var $el = $(el); + var uniqueId = self._getUniqueId(el); + + var productId = parseInt($el.find('input.product_id').val(), 10); + if (productId === self.rootProduct.product_id) { + self.rootProduct.unique_id = uniqueId; + } else { + el.dataset.parentUniqueId = self.rootProduct.unique_id; + } + }); + return def.then(function () { // This has to be triggered to compute the "out of stock" feature self._opened.then(function () { @@ -167,28 +177,21 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { getSelectedProducts: function () { var self = this; var products = [this.rootProduct]; - this.$modal.find('.js_product.in_cart:not(.main_product)').each(function (){ - var quantity = 0; + this.$modal.find('.js_product.in_cart:not(.main_product)').each(function () { var $item = $(this); - if ($item.find('input[name="add_qty"]').length){ - quantity = $item.find('input[name="add_qty"]').val(); - } else { - quantity = parseInt( - $item - .find('.optional_product_quantity span.add_qty') - .html() - .trim() - ); - } - + var quantity = parseInt($item.find('input[name="add_qty"]').val(), 10); + var parentUniqueId = this.dataset.parentUniqueId; + var uniqueId = this.dataset.uniqueId; var productCustomVariantValues = self.getCustomVariantValues($(this)); var noVariantAttributeValues = self.getNoVariantAttributeValues($(this)); products.push({ - product_id: parseInt($item.find('input.product_id').val()), - product_template_id: parseInt($item.find('input.product_template_id').val()), - quantity: quantity, - product_custom_attribute_values: productCustomVariantValues, - no_variant_attribute_values: noVariantAttributeValues + 'product_id': parseInt($item.find('input.product_id').val(), 10), + 'product_template_id': parseInt($item.find('input.product_template_id').val(), 10), + 'quantity': quantity, + 'parent_unique_id': parentUniqueId, + 'unique_id': uniqueId, + 'product_custom_attribute_values': productCustomVariantValues, + 'no_variant_attribute_values': noVariantAttributeValues }); }); @@ -219,12 +222,12 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { .find('td.td-product_name div.text-muted.small'); var description = $productDescription.html(); - $.each(this.rootProduct.product_custom_attribute_values, function (){ + $.each(this.rootProduct.product_custom_attribute_values, function () { description += '
' + this.attribute_value_name + ': ' + this.custom_value; }); - $.each(this.rootProduct.no_variant_attribute_values, function (){ - if (this.is_custom !== 'True'){ + $.each(this.rootProduct.no_variant_attribute_values, function () { + if (this.is_custom !== 'True') { description += '
' + this.attribute_name + ': ' + this.attribute_value_name; } }); @@ -264,21 +267,20 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { * @private * @param {MouseEvent} ev */ - _onAddOrRemoveOption: function (ev){ + _onAddOrRemoveOption: function (ev) { ev.preventDefault(); var self = this; var $target = $(ev.currentTarget); var $modal = $target.parents('.oe_optional_products_modal'); var $parent = $target.parents('.js_product:first'); $parent.find("a.js_add, span.js_remove").toggleClass('d-none'); - $parent.find("input.js_optional_same_quantity").val($target.hasClass("js_add") ? 1 : 0 ); $parent.find(".js_remove"); var productTemplateId = $parent.find(".product_template_id").val(); if ($target.hasClass('js_add')) { - self._onAddOption($modal, $parent ,productTemplateId); + self._onAddOption($modal, $parent, productTemplateId); } else { - self._onRemoveOption($modal, $parent ,productTemplateId); + self._onRemoveOption($modal, $parent); } self._computePriceTotal(); @@ -293,13 +295,14 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { */ _onAddOption: function ($modal, $parent, productTemplateId) { var self = this; - var $main_product = $modal.find('.js_product:first'); var $selectOptionsText = $modal.find('.o_select_options'); - // remove attribute values selection and move quantity to the correct table spot (".td-qty")) - $parent.addClass('in_cart'); - $parent.find('.td-qty').addClass('text-center'); - $parent.find('.td-qty').append($parent.find('.optional_product_quantity')); + var parentUniqueId = $parent[0].dataset.parentUniqueId; + var $optionParent = $modal.find('tr.js_product[data-unique-id="' + parentUniqueId + '"]'); + + // remove attribute values selection and update + show quantity input + $parent.find('.td-product_name').removeAttr("colspan"); + $parent.find('.td-qty').removeClass('d-none'); var productCustomVariantValues = self.getCustomVariantValues($parent); var noVariantAttributeValues = self.getNoVariantAttributeValues($parent); @@ -308,12 +311,12 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { .find('td.td-product_name div.float-left'); var description = ''; - $.each(productCustomVariantValues, function (){ + $.each(productCustomVariantValues, function () { description += '
' + this.attribute_value_name + ': ' + this.custom_value; }); - $.each(noVariantAttributeValues, function (){ - if (this.is_custom !== 'True'){ + $.each(noVariantAttributeValues, function () { + if (this.is_custom !== 'True') { description += '
' + this.attribute_name + ': ' + this.attribute_value_name; } }); @@ -326,25 +329,14 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { $productDescription.append($customAttributeValuesDescription); } - // if it's an optional product of an optional product, place it after it's parent - var parentProductId = null; - for (var productIdKey in optionalProductsMap) { - if (optionalProductsMap[productIdKey].indexOf(productTemplateId) !== -1) { - parentProductId = productIdKey; - break; - } - - $('tr:last').after($parent); - } - - if (parentProductId) { - $modal.find('.product_template_id').filter(function () { - return parentProductId === $(this).val(); - }).parents('.js_product:first').after($parent); - } else { - // else, place it after the main product - $main_product.after($parent); + // place it after its parent and its parent options + var $tmpOptionParent = $optionParent; + while ($tmpOptionParent.length) { + $optionParent = $tmpOptionParent; + $tmpOptionParent = $modal.find('tr.js_product.in_cart[data-parent-unique-id="' + $optionParent[0].dataset.uniqueId + '"]').last(); } + $optionParent.after($parent); + $parent.addClass('in_cart'); this.selectOrCreateProduct( $parent, @@ -364,19 +356,18 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { self.$el.find('input[name="add_qty"]').trigger('change'); self.triggerVariantChange($addedItem); - if ($addedItem.find(".product_template_id").length > 0) { - // we need to map this product with it's optional products to be able to: - // - remove them from the cart if the parent product is removed - // - place the optional products under their parent in - // the interface when they're added to the cart - optionalProductsMap[productTemplateId] = $addedItem - .find(".product_template_id") - .map(function () { - return $(this).val(); - }).get(); - } + // add a unique id to the new products + var parentUniqueId = $parent[0].dataset.uniqueId; + var parentQty = $parent.find('input[name="add_qty"]').val(); + $addedItem.filter('.js_product').each(function () { + var $el = $(this); + var uniqueId = self._getUniqueId(this); + this.dataset.uniqueId = uniqueId; + this.dataset.parentUniqueId = parentUniqueId; + $el.find('input[name="add_qty"]').val(parentQty); + }); - if ($selectOptionsText.nextAll('.js_product').length === 0){ + if ($selectOptionsText.nextAll('.js_product').length === 0) { // no more optional products to select -> hide the header $selectOptionsText.hide(); } @@ -389,19 +380,21 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { * @see _onAddOrRemoveOption * @param {$.Element} $modal * @param {$.Element} $parent - * @param {integer} productTemplateId */ - _onRemoveOption: function ($modal, $parent, productTemplateId){ + _onRemoveOption: function ($modal, $parent) { // restore attribute values selection + var uniqueId = $parent[0].dataset.parentUniqueId; + var qty = $modal.find('tr.js_product.in_cart[data-unique-id="' + uniqueId + '"]').find('input[name="add_qty"]').val(); $parent.removeClass('in_cart'); - $parent.find('.td-qty').removeClass('text-center'); - $parent.find('.js_remove.d-none').prepend($parent.find('.optional_product_quantity')); + $parent.find('.td-product_name').attr("colspan", 2); + $parent.find('.td-qty').addClass('d-none'); + $parent.find('input[name="add_qty"]').val(qty); $parent.find('.custom_attribute_values_description').remove(); - var $select_options_text = $modal.find('.o_select_options'); - $select_options_text.show(); + $modal.find('.o_select_options').show(); - this._removeOptionOption($modal, productTemplateId); + var productUniqueId = $parent[0].dataset.uniqueId; + this._removeOptionOption($modal, productUniqueId); $modal.find('tr:last').after($parent); }, @@ -411,26 +404,20 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { * * @private * @param {$.Element} $modal - * @param {integer} optionId The removed optional product id + * @param {integer} optionUniqueId The removed optional product id */ - _removeOptionOption: function ($modal, optionId) { + _removeOptionOption: function ($modal, optionUniqueId) { var self = this; - if (optionalProductsMap[optionId]) { - optionalProductsMap[optionId].forEach(function (productId) { - $modal.find('.product_template_id').filter(function () { - return productId === $(this).val(); - }).parents('.js_product:first').remove(); - - self._removeOptionOption($modal, productId); - }); - - delete optionalProductsMap[optionId]; - } + $modal.find('tr.js_product[data-parent-unique-id="' + optionUniqueId + '"]').each(function () { + var uniqueId = this.dataset.uniqueId; + $(this).remove(); + self._removeOptionOption($modal, uniqueId); + }); }, /** * @override */ - _onChangeCombination:function (ev, $parent, combination) { + _onChangeCombination: function (ev, $parent, combination) { $parent .find('.td-product_name .product-name') .first() @@ -441,31 +428,28 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { this._computePriceTotal(); }, /** - * When the quantity of the root product is updated, we need to update - * the quantity of all the selected optional products. + * Update price total when the quantity of a product is changed * * @private * @param {MouseEvent} ev */ - _onChangeQuantity: function (ev){ - var $quantity = $(ev.currentTarget); - var qty = parseFloat($quantity.val()); - var $modal = $quantity.parents('.oe_optional_products_modal'); - if (qty === 1) { - $modal.find(".js_items").addClass('d-none').removeClass('add_qty'); - $modal.find(".js_item").removeClass('d-none').addClass('add_qty'); - } else { - $modal.find(".js_items").removeClass('d-none').addClass('add_qty').text(qty); - $modal.find(".js_item").addClass('d-none').removeClass('add_qty'); - } + _onChangeQuantity: function (ev) { + var $product = $(ev.target.closest('tr.js_product')); + var qty = parseFloat($(ev.currentTarget).val()); - this.rootProduct.quantity = qty; + var uniqueId = $product[0].dataset.uniqueId; + this.$el.find('tr.js_product:not(.in_cart)[data-parent-unique-id="' + uniqueId + '"] input[name="add_qty"]').each(function () { + $(this).val(qty); + }); - if (this._triggerPriceUpdateOnChangeQuantity()){ + if (this._triggerPriceUpdateOnChangeQuantity()) { this.onChangeAddQuantity(ev); } - - this.trigger('update_quantity', qty); + if ($product.hasClass('main_product')) { + this.rootProduct.quantity = qty; + } + this.trigger('update_quantity', this.rootProduct.quantity); + this._computePriceTotal(); }, /** @@ -473,10 +457,10 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { * we need to refresh the total price row */ _computePriceTotal: function () { - if (this.$modal.find('.js_price_total').length){ + if (this.$modal.find('.js_price_total').length) { var price = 0; - var quantity = parseInt(this.$modal.find('input[name="add_qty"]').first().val()); - this.$modal.find('.js_product.in_cart').each(function (){ + this.$modal.find('.js_product.in_cart').each(function () { + var quantity = parseInt($(this).find('input[name="add_qty"]').first().val(), 10); price += parseFloat($(this).find('.js_raw_price').html()) * quantity; }); @@ -493,7 +477,20 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { */ _triggerPriceUpdateOnChangeQuantity: function () { return true; - } + }, + /** + * Returns a unique id for `$el`. + * + * @private + * @param {Element} el + * @returns {integer} + */ + _getUniqueId: function (el) { + if (!el.dataset.uniqueId) { + el.dataset.uniqueId = parseInt(_.uniqueId(), 10); + } + return el.dataset.uniqueId; + }, }); return OptionalProductsModal; diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js index 8fe2b3d207e..c3ccafaf427 100644 --- a/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js +++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_optional_products_ui.js @@ -19,33 +19,61 @@ tour.register('sale_product_configurator_optional_products_tour', { trigger: "a:contains('Add a product')" }, { trigger: 'div[name="product_template_id"] input', - run: function (){ + run: function () { var $input = $('div[name="product_template_id"] input'); $input.click(); - $input.val('Office Chair Black'); + $input.val('Customizable Desk'); var keyDownEvent = jQuery.Event("keydown"); keyDownEvent.which = 42; $input.trigger(keyDownEvent); } }, { - trigger: 'ul.ui-autocomplete a:contains("Office Chair Black")', + trigger: 'ul.ui-autocomplete a:contains("Customizable Desk")', run: 'click' }, { - trigger: '.js_add' + trigger: '.o_sale_product_configurator_add' }, { - trigger: '.js_add_cart_json .fa-plus' + trigger: 'tr:has(.td-product_name:contains("Office Chair Black")) .js_add', +}, { + trigger: 'tr:has(.td-product_name:contains("Customizable Desk")) .fa-plus' +}, { + trigger: 'tr:has(.td-product_name:contains("Chair floor protection")) .js_add', +}, { + content: 'Is below its parent 1', + trigger: 'tr:has(.td-product_name:contains("Office Chair Black")) + tr:has(.td-product_name:contains("Chair floor protection"))' +}, { + trigger: 'tr:has(.td-product_name:contains("Conference Chair")) .js_add', +}, { + trigger: 'tr:has(.td-product_name:contains("Conference Chair")) .fa-minus' +}, { + trigger: 'tr:has(.td-product_name:contains("Chair floor protection")) .js_add', +}, { + content: 'Is below its parent 2', + trigger: 'tr:has(.td-product_name:contains("Conference Chair")) + tr:has(.td-product_name:contains("Chair floor protection"))' }, { trigger: 'button span:contains(Confirm)', extra_trigger: '.oe_optional_products_modal', run: 'click' }, { - trigger: 'tr:has(td.o_data_cell:contains("Office Chair Black")) td.o_data_cell:contains("2.0")', + trigger: 'tr:has(td.o_data_cell:contains("Customizable Desk")) td.o_data_cell:contains("2.0")', extra_trigger: 'div[name="order_line"]', - run: function (){} // check added product + run: function () {}, // check added product }, { - trigger: 'tr:has(td.o_data_cell:contains("Chair floor protection")) td.o_data_cell:contains("2.0")', + trigger: 'tr:has(td.o_data_cell:contains("Office Chair Black")) td.o_data_cell:contains("1.0")', extra_trigger: 'div[name="order_line"]', - run: function (){} // check added product + run: function () {}, // check added product +}, { + trigger: 'tr:has(td.o_data_cell:contains("Conference Chair")) td.o_data_cell:contains("1.0")', + extra_trigger: 'div[name="order_line"]', + run: function () {}, // check added product +}, { + trigger: 'tr:has(td.o_data_cell:contains("Chair floor protection")):nth(0) td.o_data_cell:contains("1.0")', + extra_trigger: 'div[name="order_line"]', + run: function () {}, // check added product +}, { + trigger: 'tr:has(td.o_data_cell:contains("Chair floor protection")):nth(1) td.o_data_cell:contains("1.0")', + extra_trigger: 'div[name="order_line"]', + run: function () {}, // check added product }]); }); diff --git a/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py index d4940006d6d..fe0353499d7 100644 --- a/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py +++ b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py @@ -160,10 +160,13 @@ class TestUi(odoo.tests.HttpCase): if the main product does not have variants. """ - # add an optional product to the office chair for test purposes + # add an optional product to the office chair and the custo desk for testing purposes office_chair = self.env.ref('product.product_product_12') + custo_desk = self.env.ref('product.product_product_4') office_chair.update({ 'optional_product_ids': [(6, 0, [self.env.ref('sale_product_configurator.product_product_1_product_template').id])] }) - + custo_desk.update({ + 'optional_product_ids': [(6, 0, [self.env.ref('product.product_product_12_product_template').id, self.env.ref('product.product_product_11_product_template').id])] + }) self.start_tour("/web", 'sale_product_configurator_optional_products_tour', login="admin") diff --git a/addons/sale_product_configurator/views/templates.xml b/addons/sale_product_configurator/views/templates.xml index 4a849dc7846..5386d4bd787 100644 --- a/addons/sale_product_configurator/views/templates.xml +++ b/addons/sale_product_configurator/views/templates.xml @@ -37,7 +37,7 @@ "display_currency": (pricelist or product).currency_id }'/> -
+
-
+
+ +
+
+ +
+ +
+ +
+
+

Option not available

- Add to cart - 1 5
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 49accd853c1..9ae5755a2f5 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -147,6 +147,9 @@ class SaleOrder(models.Model): product_context = dict(self.env.context) product_context.setdefault('lang', self.sudo().partner_id.lang) SaleOrderLineSudo = self.env['sale.order.line'].sudo().with_context(product_context) + # change lang to get correct name of attributes/values + product_with_context = self.env['product.product'].with_context(product_context) + product = product_with_context.browse(int(product_id)) try: if add_qty: @@ -168,9 +171,6 @@ class SaleOrder(models.Model): # Create line if no line with product_id can be located if not order_line: - # change lang to get correct name of attributes/values - product = self.env['product.product'].with_context(product_context).browse(int(product_id)) - if not product: raise UserError(_("The given product does not exist therefore it cannot be added to cart.")) @@ -223,11 +223,6 @@ class SaleOrder(models.Model): # create the line order_line = SaleOrderLineSudo.create(values) - # Generate the description with everything. This is done after - # creating because the following related fields have to be set: - # - product_no_variant_attribute_value_ids - # - product_custom_attribute_value_ids - order_line.name = order_line.get_sale_order_line_multiline_description_sale(product) try: order_line._compute_tax_id() @@ -245,7 +240,12 @@ class SaleOrder(models.Model): # Remove zero of negative lines if quantity <= 0: + linked_line = order_line.linked_line_id order_line.unlink() + if linked_line: + # update description of the parent + linked_product = product_with_context.browse(linked_line.product_id.id) + linked_line.name = linked_line.get_sale_order_line_multiline_description_sale(linked_product) else: # update line no_variant_attributes_price_extra = [ptav.price_extra for ptav in order_line.product_no_variant_attribute_value_ids] @@ -258,7 +258,8 @@ class SaleOrder(models.Model): 'date': order.date_order, 'pricelist': order.pricelist_id.id, }) - product = self.env['product.product'].with_context(product_context).browse(product_id) + product_with_context = self.env['product.product'].with_context(product_context) + product = product_with_context.browse(product_id) values['price_unit'] = self.env['account.tax']._fix_tax_included_price_company( order_line._get_display_price(product), order_line.product_id.taxes_id, @@ -273,13 +274,17 @@ class SaleOrder(models.Model): linked_line = SaleOrderLineSudo.browse(kwargs['linked_line_id']) order_line.write({ 'linked_line_id': linked_line.id, - 'name': order_line.name + "\n" + _("Option for:") + ' ' + linked_line.product_id.display_name, }) - linked_line.write({"name": linked_line.name + "\n" + _("Option:") + ' ' + order_line.product_id.display_name}) + linked_product = product_with_context.browse(linked_line.product_id.id) + linked_line.name = linked_line.get_sale_order_line_multiline_description_sale(linked_product) + # Generate the description with everything. This is done after + # creating because the following related fields have to be set: + # - product_no_variant_attribute_value_ids + # - product_custom_attribute_value_ids + # - linked_line_id + order_line.name = order_line.get_sale_order_line_multiline_description_sale(product) option_lines = self.order_line.filtered(lambda l: l.linked_line_id.id == order_line.id) - for option_line_id in option_lines: - self._cart_update(option_line_id.product_id.id, option_line_id.id, add_qty, set_qty, **kwargs) return {'line_id': order_line.id, 'quantity': quantity, 'option_ids': list(set(option_lines.ids))} @@ -357,6 +362,14 @@ class SaleOrderLine(models.Model): linked_line_id = fields.Many2one('sale.order.line', string='Linked Order Line', domain="[('order_id', '!=', order_id)]", ondelete='cascade') option_line_ids = fields.One2many('sale.order.line', 'linked_line_id', string='Options Linked') + def get_sale_order_line_multiline_description_sale(self, product): + description = super(SaleOrderLine, self).get_sale_order_line_multiline_description_sale(product) + if self.linked_line_id: + description += "\n" + _("Option for: %s") % self.linked_line_id.product_id.display_name + if self.option_line_ids: + description += "\n" + '\n'.join([_("Option: %s") % option_line.product_id.display_name for option_line in self.option_line_ids]) + return description + @api.depends('product_id.display_name') def _compute_name_short(self): """ Compute a short name for this sale order line, to be used on the website where we don't have much space. diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 08c7f8775cf..9ce94c90448 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -706,7 +706,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { * * @override */ - onChangeVariant: function (ev, data) { + onChangeVariant: function (ev) { var $component = $(ev.currentTarget).closest('.js_product'); $component.find('input').each(function () { var $el = $(this); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 26179b736ea..c16b7862491 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -616,7 +616,7 @@