[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) <qsm@odoo.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)'
|
||||
}, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'];
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user