From 51ef8514eaddaebbdfee73230cfe955735d02f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Tue, 21 May 2019 09:50:53 +0000 Subject: [PATCH] [FIX] product,sale,website_sale: compute combination possible in rpc This has several advantages: - make the code easier to maintain by having the logic only on the python model - prevent price flickering when changing from/to an impossible combination - fix the UI for the issue mentioned in the previous commit/the opw - make the code much faster when using many combinations, by not requiring to browse and to return every existing and every archived variants The opportunity is also taken to properly throttle the rpc and to use "DropMisordered" on its result: - fix all the other remaining flickering when changing quantity or combination - fix the server being spammed of rpc when using the configurator modal, but also in a lot of other cases such as incrementing the quantity, ... - fix UI consistency by ensuring the result of the last rpc is always the one displayed, obsolete rpc returns are ignored By computing the possibility of the combination asynchronously, the two drawbacks are: - the UI is a bit less reactive than in the previously the best case scenario, but this almost completely mitigated by debouncing with a leading call - it is possible for a quick user to click on "add to cart" on an impossible combination before the return of the rpc, but this is not really an issue because when the product will be added to cart there will a check and the closest possible combination will be selected automatically instead Finally the event of the configurator mixin and the widgets using it have been cleaned to avoid the duplication of the rpc in the first place. opw-1959317 PR: #32691 Signed-off-by: Quentin Smetz (qsm) --- addons/product/models/product_template.py | 46 +++-- .../sale/controllers/product_configurator.py | 9 +- .../src/js/product_configurator_mixin.js | 178 +++++++++--------- .../src/js/product_configurator_modal.js | 37 ++-- .../src/js/product_configurator_renderer.js | 21 ++- .../tests/product_configurator_test_ui.js | 16 +- .../static/src/js/website_sale.js | 40 ++-- .../static/src/js/website_sale_options.js | 5 +- 8 files changed, 182 insertions(+), 170 deletions(-) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 5fbe41d8b9b..915dfd6c555 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -132,8 +132,10 @@ class ProductTemplate(models.Model): compute="_compute_valid_attributes", string='Valid Product Attribute Values Without No Variant Attributes', help="Technical compute") valid_product_attribute_wnva_ids = fields.Many2many('product.attribute', compute="_compute_valid_attributes", string='Valid Product Attributes Without No Variant Attributes', help="Technical compute") + # valid_archived_variant_ids deprecated valid_archived_variant_ids = fields.Many2many('product.product', - compute="_compute_valid_attributes", string='Valid Archived Variants', help="Technical compute") + compute="_compute_valid_archived_variant_ids", string='Valid Archived Variants', help="Technical compute") + # valid_existing_variant_ids deprecated valid_existing_variant_ids = fields.Many2many('product.product', compute="_compute_valid_existing_variant_ids", string='Valid Existing Variants', help="Technical compute") @@ -586,8 +588,6 @@ class ProductTemplate(models.Model): For what is considered an archived variant, see `_has_valid_attributes`. """ - archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)]) - # prefetch self.mapped('attribute_line_ids.value_ids.id') self.mapped('attribute_line_ids.attribute_id.create_variant') @@ -602,6 +602,12 @@ class ProductTemplate(models.Model): record.valid_product_attribute_ids = record.valid_product_template_attribute_line_ids.mapped('attribute_id') record.valid_product_attribute_wnva_ids = record.valid_product_template_attribute_line_wnva_ids.mapped('attribute_id') + @api.multi + def _compute_valid_archived_variant_ids(self): + """This compute is done outside of `_compute_valid_attributes` because + it is often not needed, and it can be very bad on performance.""" + archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)]) + for record in self: valid_value_ids = record.valid_product_attribute_value_wnva_ids valid_attribute_ids = record.valid_product_attribute_wnva_ids @@ -693,23 +699,23 @@ class ProductTemplate(models.Model): :return: dict of exclusions - exclusions: from this product itself + - parent_combination: ids of the given parent_combination - parent_exclusions: from the parent_combination - - archived_combinations: variants that are archived - - existing_combinations: variants that are existing (as opposed to - deleted, because deleted need to be considered impossible if - there are no dynamic attributes). - - has_dynamic_attributes: whether there is a dynamic attribute - - no_variant_product_template_attribute_value_ids: values that are - no_variant + - archived_combinations: deprecated + - existing_combinations: deprecated + - has_dynamic_attributes: deprecated + - no_variant_product_template_attribute_value_ids: deprecated """ self.ensure_one() + parent_combination = parent_combination or self.env['product.template.attribute.value'] return { 'exclusions': self._get_own_attribute_exclusions(), 'parent_exclusions': self._get_parent_attribute_exclusions(parent_combination), - 'archived_combinations': self._get_archived_combinations(), + 'parent_combination': parent_combination.ids, + 'archived_combinations': [], 'has_dynamic_attributes': self.has_dynamic_attributes(), - 'existing_combinations': self._get_existing_combinations(), - 'no_variant_product_template_attribute_value_ids': self._get_no_variant_product_template_attribute_values(), + 'existing_combinations': [], + 'no_variant_product_template_attribute_value_ids': [], } @api.multi @@ -762,29 +768,21 @@ class ProductTemplate(models.Model): @api.multi def _get_archived_combinations(self): + """Deprecated""" self.ensure_one() - """Get archived combinations. - - Array, each element is an array with ids of an archived combination. - """ return [archived_variant.product_template_attribute_value_ids.ids for archived_variant in self.valid_archived_variant_ids] @api.multi def _get_existing_combinations(self): + """Deprecated""" self.ensure_one() - """Get existing combinations. - - Needed because when not using dynamic attributes, the combination is - not ok if it doesn't exist (= if the variant has been deleted). - - Array, each element is an array with ids of an existing combination. - """ return [variant.product_template_attribute_value_ids.ids for variant in self.valid_existing_variant_ids] @api.multi def _get_no_variant_product_template_attribute_values(self): + """Deprecated""" self.ensure_one() product_template_attribute_values = self._get_valid_product_template_attribute_lines().mapped('product_template_value_ids') return product_template_attribute_values.filtered( diff --git a/addons/sale/controllers/product_configurator.py b/addons/sale/controllers/product_configurator.py index eea3dfe67fd..30d37fdde14 100644 --- a/addons/sale/controllers/product_configurator.py +++ b/addons/sale/controllers/product_configurator.py @@ -44,7 +44,14 @@ class ProductConfiguratorController(http.Controller): ProductTemplate = request.env['product.template'] if 'context' in kw: ProductTemplate = ProductTemplate.with_context(**kw.get('context')) - return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + product_template = ProductTemplate.browse(int(product_template_id)) + res = product_template._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + if 'parent_combination' in kw: + parent_combination = request.env['product.template.attribute.value'].browse(kw.get('parent_combination')) + res.update({ + 'is_combination_possible': product_template._is_combination_possible(combination=combination, parent_combination=parent_combination), + }) + return res @http.route(['/product_configurator/create_product_variant'], type='json', auth="user", methods=['POST']) def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 0d49a4f245e..bb72b4b428a 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -1,6 +1,7 @@ odoo.define('sale.ProductConfiguratorMixin', function (require) { 'use strict'; +var concurrency = require('web.concurrency'); var core = require('web.core'); var utils = require('web.utils'); var ajax = require('web.ajax'); @@ -8,11 +9,11 @@ var _t = core._t; var ProductConfiguratorMixin = { events: { - 'click .css_attribute_color input': '_onChangeColorAttribute', + 'change .css_attribute_color input': '_onChangeColorAttribute', 'change .main_product:not(.in_cart) input.js_quantity': 'onChangeAddQuantity', - 'click button.js_add_cart_json': 'onClickAddCartJSON', 'change [data-attribute_exclusions]': 'onChangeVariant' }, + // isSelectedVariantAllowed deprecated isSelectedVariantAllowed: true, //-------------------------------------------------------------------------- @@ -20,23 +21,11 @@ var ProductConfiguratorMixin = { //-------------------------------------------------------------------------- /** - * When a product is added or when the quantity is changed, - * we need to refresh the total price row - * TODO awa: add a container context to avoid global selectors ? + * This method is only useful in the configurator modal. + * + * @deprecated */ - computePriceTotal: function () { - if ($('.js_price_total').length){ - var price = 0; - $('.js_product.in_cart').each(function (){ - var quantity = parseInt($('input[name="add_qty"]').first().val()); - price += parseFloat($(this).find('.js_raw_price').html()) * quantity; - }); - - $('.js_price_total .oe_currency_value').html( - this._priceToStr(parseFloat(price)) - ); - } - }, + computePriceTotal: function () {}, /** * When a variant is changed, this will check: @@ -49,6 +38,20 @@ var ProductConfiguratorMixin = { * @param {MouseEvent} ev */ onChangeVariant: function (ev) { + var $parent = $(ev.target).closest('.js_product'); + if (!$parent.data('uniqueId')) { + $parent.data('uniqueId', _.uniqueId()); + } + this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev); + }, + /** + * @see onChangeVariant + * + * @private + * @param {Event} ev + * @returns {Deferred} + */ + _getCombinationInfo: function (ev) { var self = this; var $component; @@ -64,16 +67,21 @@ var ProductConfiguratorMixin = { var qty = $component.find('input[name="add_qty"]').val(); var $parent = $(ev.target).closest('.js_product'); + 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); - ajax.jsonRpc(this._getUri('/product_configurator/get_combination_info'), 'call', { - product_template_id: parseInt($parent.find('.product_template_id').val()), - product_id: this._getProductId($parent), - combination: combination, - add_qty: parseInt(qty), - pricelist_id: this.pricelistId || false, + return ajax.jsonRpc(this._getUri('/product_configurator/get_combination_info'), 'call', { + 'product_template_id': productTemplateId, + 'product_id': this._getProductId($parent), + 'combination': combination, + 'add_qty': parseInt(qty), + 'pricelist_id': this.pricelistId || false, + 'parent_combination': parentCombination, }).then(function (combinationData) { self._onChangeCombination(ev, $parent, combinationData); }); @@ -149,10 +157,13 @@ var ProductConfiguratorMixin = { var $input = $link.closest('.input-group').find("input"); var min = parseFloat($input.data("min") || 0); var max = parseFloat($input.data("max") || Infinity); - var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10); + var previousQty = parseFloat($input.val() || 0, 10); + var quantity = ($link.has(".fa-minus").length ? -1 : 1) + previousQty; var newQty = quantity > min ? (quantity < max ? quantity : max) : min; - $input.val(newQty).trigger('change'); + if (newQty !== previousQty) { + $input.val(newQty).trigger('change'); + } return false; }, @@ -343,23 +354,6 @@ var ProductConfiguratorMixin = { * @param {Array} combination the selected combination of product attribute values */ _checkExclusions: function ($parent, combination) { - - function areCombinationsEqual(c1, c2) { - return c1.length === c2.length && _.every(c1, function (ptav) { - return c2.indexOf(ptav) > -1; - }); - } - - function isCombinationInList(c1, list) { - return _.some(list, function (c2) { - return areCombinationsEqual(c1, c2); - }); - } - - function isPtavInCombination(ptav, combination) { - return combination.indexOf(ptav) > -1; - } - var self = this; var combinationData = $parent .find('ul[data-attribute_exclusions]') @@ -367,17 +361,6 @@ var ProductConfiguratorMixin = { $parent.find('option, input, label').removeClass('css_not_available'); - var disable = false; - - // compatibility 12.0 - var filteredCombination = combination; - if (combinationData.no_variant_product_template_attribute_value_ids !== undefined) { - var no_variants = combinationData.no_variant_product_template_attribute_value_ids; - filteredCombination = _.filter(combination, function (ptav) { - return !isPtavInCombination(ptav, no_variants); - }); - } - // exclusion rules: array of ptav // for each of them, contains array with the other ptav they exclude if (combinationData.exclusions) { @@ -386,11 +369,6 @@ var ProductConfiguratorMixin = { if (combinationData.exclusions.hasOwnProperty(current_ptav)) { // for each exclusion of the current attribute: _.each(combinationData.exclusions[current_ptav], function (excluded_ptav) { - // disable if it excludes any other attribute already in the combination - if (isPtavInCombination(excluded_ptav, combination)) { - disable = true; - } - // disable the excluded input (even when not already selected) // to give a visual feedback before click self._disableInput($parent, excluded_ptav); @@ -401,37 +379,11 @@ var ProductConfiguratorMixin = { // parent exclusions (tell which attributes are excluded from parent) _.each(combinationData.parent_exclusions, function (ptav) { - if (isPtavInCombination(ptav, combination)) { - disable = true; - } // disable the excluded input (even when not already selected) // to give a visual feedback before click self._disableInput($parent, ptav); }); - - // archived variants - if (isCombinationInList(filteredCombination, combinationData.archived_combinations)) { - disable = true; - } - - // if not using dynamic attributes, exclude variants that are deleted - if (filteredCombination.length && // compatibility 12.0 list view of variants - combinationData.has_dynamic_attributes === false && - combinationData.existing_combinations !== undefined && - !isCombinationInList(filteredCombination, combinationData.existing_combinations) - ) { - disable = true; - } - - this.isSelectedVariantAllowed = !disable; - $parent.toggleClass('css_not_available', disable); - $parent.find("#add_to_cart").toggleClass('disabled', disable); - $parent - .parents('.modal') - .find('.o_sale_product_configurator_add') - .toggleClass('disabled', disable); }, - /** * Extracted to a method to be extendable by other modules * @@ -440,7 +392,6 @@ var ProductConfiguratorMixin = { _getProductId: function ($parent) { return parseInt($parent.find('.product_id').val()); }, - /** * Will disable the input/option that refers to the passed attributeValueId. * This is used for showing the user that some combinations are not available. @@ -455,7 +406,6 @@ var ProductConfiguratorMixin = { $input.addClass('css_not_available'); $input.closest('label').addClass('css_not_available'); }, - /** * @see onChangeVariant * @@ -469,6 +419,15 @@ var ProductConfiguratorMixin = { var $price = $parent.find(".oe_price:first .oe_currency_value"); var $default_price = $parent.find(".oe_default_price:first .oe_currency_value"); var $optional_price = $parent.find(".oe_optional:first .oe_currency_value"); + + var isCombinationPossible = this.isSelectedVariantAllowed; + + if (combination.is_combination_possible !== undefined) { + isCombinationPossible = combination.is_combination_possible; + } + + this._toggleDisable($parent, isCombinationPossible); + $price.html(self._priceToStr(combination.price)); $default_price.html(self._priceToStr(combination.list_price)); @@ -505,7 +464,8 @@ var ProductConfiguratorMixin = { $parent.closest(rootComponentSelectors.join(', ')), combination.product_id, combination.product_template_id, - combination.carousel + combination.carousel, + isCombinationPossible ); } @@ -525,7 +485,6 @@ var ProductConfiguratorMixin = { .first() .html(combination.price); - this.computePriceTotal(); this.handleCustomValues($(ev.target)); }, @@ -546,7 +505,46 @@ var ProductConfiguratorMixin = { formatted[0] = utils.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); }, - + /** + * Returns a throttled `_getCombinationInfo` with a leading and a trailing + * call, which is memoized per `uniqueId`, and for which previous results + * are dropped. + * + * The uniqueId is needed because on the configurator modal there might be + * multiple elements triggering the rpc at the same time, and we need each + * individual product rpc to be executed, but only once per individual + * product. + * + * The leading execution is to keep good reactivity on the first call, for + * a better user experience. The trailing is because ultimately only the + * information about the last selected combination is useful. All + * intermediary rpc can be ignored and are therefore best not done at all. + * + * The DropMisordered is to make sure slower rpc are ignored if the result + * of a newer rpc has already been received. + * + * @private + * @param {string} uniqueId + * @returns {function} + */ + _throttledGetCombinationInfo: _.memoize(function (uniqueId) { + var dropMisordered = new concurrency.DropMisordered(); + var _getCombinationInfo = _.throttle(this._getCombinationInfo.bind(this), 500); + return function (ev) { + return dropMisordered.add(_getCombinationInfo(ev)); + }; + }), + /** + * Toggles the disabled class depending on the $parent element + * and the possibility of the current combination. + * + * @private + * @param {$.Element} $parent + * @param {boolean} isCombinationPossible + */ + _toggleDisable: function ($parent, isCombinationPossible) { + $parent.toggleClass('css_not_available', !isCombinationPossible); + }, /** * Updates the product image. * This will use the productId if available or will fallback to the productTemplateId. diff --git a/addons/sale/static/src/js/product_configurator_modal.js b/addons/sale/static/src/js/product_configurator_modal.js index 60544ada7c4..8b0410c956e 100644 --- a/addons/sale/static/src/js/product_configurator_modal.js +++ b/addons/sale/static/src/js/product_configurator_modal.js @@ -13,20 +13,12 @@ var optionalProductsMap = {}; var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixin, { events: _.extend({}, Dialog.prototype.events, ProductConfiguratorMixin.events, { 'click a.js_add, a.js_remove': '_onAddOrRemoveOption', - 'change .in_cart.main_product input.js_quantity': '_onChangeQuantity' + 'click button.js_add_cart_json': 'onClickAddCartJSON', + 'change .in_cart.main_product input.js_quantity': '_onChangeQuantity', }), /** * Initializes the optional products modal * - * If the "isWebsite" param is true, will also disable the following events: - * - change [data-attribute_exclusions] - * - click button.js_add_cart_json - * - * This has to be done because those events are already registered at the "website_sale" - * component level. - * This modal is part of the form that has these events registered and we - * want to avoid duplicates. - * * @override * @param {$.Element} parent The parent container * @param {Object} params @@ -67,10 +59,6 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi this.pricelistId = params.pricelistId; this.isWebsite = params.isWebsite; this.dialogClass = 'oe_optional_products_modal' + (params.isWebsite ? ' oe_website_sale' : ''); - - if (this.isWebsite) { - delete this.events['click button.js_add_cart_json']; - } this._productImageField = 'image_medium'; }, /** @@ -156,6 +144,25 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi // Public //-------------------------------------------------------------------------- + /** + * Computes and updates the total price, useful when a product is added or + * when the quantity is changed. + * TODO awa: add a container context to avoid global selectors ? + */ + computePriceTotal: function () { + if ($('.js_price_total').length) { + var price = 0; + $('.js_product.in_cart').each(function () { + var quantity = parseInt($('input[name="add_qty"]').first().val()); + price += parseFloat($(this).find('.js_raw_price').html()) * quantity; + }); + + $('.js_price_total .oe_currency_value').html( + this._priceToStr(parseFloat(price)) + ); + } + ProductConfiguratorMixin.computePriceTotal.apply(this, arguments); + }, /** * Returns the list of selected products. * The root product is added on top of the list. @@ -450,6 +457,8 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi .text(combination.display_name); ProductConfiguratorMixin._onChangeCombination.apply(this, arguments); + + this.computePriceTotal(); }, /** * When the quantity of the root product is updated, we need to update diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale/static/src/js/product_configurator_renderer.js index 899656c8065..d2cebf8794e 100644 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ b/addons/sale/static/src/js/product_configurator_renderer.js @@ -5,6 +5,10 @@ var FormRenderer = require('web.FormRenderer'); var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin'); var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMixin ,{ + + events: _.extend({}, FormRenderer.prototype.events, ProductConfiguratorMixin.events, { + 'click button.js_add_cart_json': 'onClickAddCartJSON', + }), /** * @override */ @@ -44,7 +48,22 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMix $configuratorHtml.appendTo($configuratorContainer); this.triggerVariantChange($configuratorContainer); - } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Toggles the add button depending on the possibility of the current + * combination. + * + * @override + */ + _toggleDisable: function ($parent, isCombinationPossible) { + ProductConfiguratorMixin._toggleDisable.apply(this, arguments); + $parent.parents('.modal').find('.o_sale_product_configurator_add').toggleClass('disabled', !isCombinationPossible); + }, }); return ProductConfiguratorFormRenderer; diff --git a/addons/sale_management/static/tests/product_configurator_test_ui.js b/addons/sale_management/static/tests/product_configurator_test_ui.js index 03f5a0a8bbc..0c98a46c166 100644 --- a/addons/sale_management/static/tests/product_configurator_test_ui.js +++ b/addons/sale_management/static/tests/product_configurator_test_ui.js @@ -30,16 +30,7 @@ tour.register('sale_product_configurator_tour', { run: 'click' }, { trigger: '.configurator_container span:contains("Steel")', - run: function () { - $('input.product_id').change(function () { - var request_count = 0; - if ($('.o_sale_product_configurator_add').attr('request_count')) { - request_count = parseInt($('.o_sale_product_configurator_add').attr('request_count')); - } - request_count++; - $('.o_sale_product_configurator_add').attr('request_count', request_count); - }); - } + run: function () {}, }, { trigger: '.configurator_container span:contains("Aluminium")', run: 'click' @@ -49,10 +40,9 @@ tour.register('sale_product_configurator_tour', { }, { trigger: 'input[data-value_name="Black"]' }, { - trigger: 'input[data-value_name="White"]' + trigger: '.o_sale_product_configurator_add.disabled' }, { - trigger: '.o_sale_product_configurator_add[request_count="3"]', - run: function (){} // used to sync with "get_combination_info" completion + trigger: 'input[data-value_name="White"]' }, { trigger: '.o_sale_product_configurator_add:not(.disabled)' }, { diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 87f91a5b045..cf85d706188 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -145,15 +145,13 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM 'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute', 'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel', 'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel', - 'change .css_attribute_color input': '_onChangeColorAttribute', 'click .show_coupon': '_onClickShowCoupon', 'submit .o_website_sale_search': '_onSubmitSaleSearch', 'change select[name="country_id"]': '_onChangeCountry', 'change #shipping_use_same': '_onChangeShippingUseSame', 'click .toggle_summary': '_onToggleSummary', 'click input.js_product_change': 'onChangeVariant', - // dirty fix: prevent options modal events to be triggered and bubbled - 'change oe_optional_products_modal [data-attribute_exclusions]': 'onChangeVariant', + 'change .js_main_product [data-attribute_exclusions]': 'onChangeVariant', }, /** @@ -166,6 +164,9 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM this._changeCountry = _.debounce(this._changeCountry.bind(this), 500); this.isWebsite = true; + + delete this.events['change .main_product:not(.in_cart) input.js_quantity']; + delete this.events['change [data-attribute_exclusions]']; }, /** * @override @@ -388,10 +389,14 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM * @override * @private */ - _updateProductImage: function ($productContainer, productId, productTemplateId, new_carousel) { + _updateProductImage: function ($productContainer, productId, productTemplateId, new_carousel, isCombinationPossible) { var $img; var $carousel = $productContainer.find('#o-carousel-product'); + if (isCombinationPossible === undefined) { + isCombinationPossible = this.isSelectedVariantAllowed; + } + if (new_carousel) { // When using the web editor, don't reload this or the images won't // be able to be edited depending on if this is done loading before @@ -434,7 +439,7 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM } } - $carousel.toggleClass('css_not_available', !this.isSelectedVariantAllowed); + $carousel.toggleClass('css_not_available', !isCombinationPossible); }, //-------------------------------------------------------------------------- @@ -540,15 +545,6 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM var dec = value % 1; $price.html(value + (dec < 0.01 ? ".00" : (dec < 1 ? "0" : "") )); }, - /** - * @private - * @param {Event} ev - */ - _onChangeColorAttribute: function (ev) { // highlight selected color - $('.css_attribute_color').removeClass("active") - .filter(':has(input:checked)') - .addClass("active"); - }, /** * @private * @param {Event} ev @@ -592,18 +588,14 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM $('.ship_to_other').toggle(!$(ev.currentTarget).prop('checked')); }, /** - * @override + * Toggles the add to cart button depending on the possibility of the + * current combination. * - * Dirty fix: prevent options modal events to be triggered and bubbled + * @override */ - onChangeVariant: function (ev, data) { - var $originPath = ev.originalEvent && Array.isArray(ev.originalEvent.path) ? $(ev.originalEvent.path) : $(); - var $container = data && data.$container ? data.$container : $(); - if ($originPath.add($container).hasClass('oe_optional_products_modal')) { - ev.stopPropagation(); - return; - } - return ProductConfiguratorMixin.onChangeVariant.apply(this, arguments); + _toggleDisable: function ($parent, isCombinationPossible) { + ProductConfiguratorMixin._toggleDisable.apply(this, arguments); + $parent.find("#add_to_cart").toggleClass('disabled', !isCombinationPossible); }, /** * @private diff --git a/addons/website_sale/static/src/js/website_sale_options.js b/addons/website_sale/static/src/js/website_sale_options.js index c0adc909bce..d05d9f9fbdb 100644 --- a/addons/website_sale/static/src/js/website_sale_options.js +++ b/addons/website_sale/static/src/js/website_sale_options.js @@ -23,10 +23,9 @@ sAnimations.registry.WebsiteSaleOptions = sAnimations.Class.extend(ProductConfig this.isWebsite = true; + delete this.events['change .css_attribute_color input']; + delete this.events['change .main_product:not(.in_cart) input.js_quantity']; delete this.events['change [data-attribute_exclusions]']; - delete this.events['click input.js_product_change']; - delete this.events['change input.js_quantity']; - delete this.events['click button.js_add_cart_json']; }, //--------------------------------------------------------------------------