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 @@