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']; }, //--------------------------------------------------------------------------