[IMP] (website_)sale: Add custom values for product attributes
Purpose ======= Product attrivutes can now have custom values. These values are specified by the user during the SO creation. They are also available on the webshop frontend. Specification ============= add a new mode to generate variants as they are added to the order/cart - ability to define attribute extra prices & exclude combinations
This commit is contained in:
@@ -52,19 +52,20 @@ class ProductAttributeValue(models.Model):
|
||||
|
||||
class ProductProductAttributeValue(models.Model):
|
||||
_name = "product.product.attribute.value"
|
||||
_order = 'sequence, attribute_id, id'
|
||||
|
||||
name = fields.Char('Value', related="product_attribute_value_id.name")
|
||||
product_attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute Value', required=True, ondelete='cascade', index=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True, ondelete='cascade', index=True)
|
||||
attribute_id = fields.Many2one('product.attribute', string='Attribute', related="product_attribute_value_id.attribute_id")
|
||||
sequence = fields.Integer('product.sequence', related="product_attribute_value_id.sequence")
|
||||
sequence = fields.Integer('Sequence', related="product_attribute_value_id.sequence")
|
||||
price_extra = fields.Float(
|
||||
string='Attribute Price Extra', default=0.0, digits=dp.get_precision('Product Price'),
|
||||
help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.")
|
||||
exclude_for = fields.Many2many(
|
||||
'product.attribute.filter.line', string="Exclude for", relation="product_attribute_value_exclusion",
|
||||
help="""A list of product and attribute values that you want to exclude for this product's attribue value.
|
||||
Also applies on optionnal and accessory products.""")
|
||||
help="""A list of product and attribute values that you want to exclude for this product's attribute value.
|
||||
Also applies on optional and accessory products.""")
|
||||
|
||||
@api.multi
|
||||
def name_get(self):
|
||||
@@ -90,7 +91,12 @@ class ProductAttributeLine(models.Model):
|
||||
product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True)
|
||||
attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='restrict', required=True)
|
||||
value_ids = fields.Many2many('product.attribute.value', string='Attribute Values')
|
||||
product_value_ids = fields.Many2many('product.product.attribute.value', string='Product Attribute Values', compute="_set_product_value_ids")
|
||||
product_value_ids = fields.Many2many(
|
||||
'product.product.attribute.value',
|
||||
string='Product Attribute Values',
|
||||
compute="_set_product_value_ids",
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.constrains('value_ids', 'attribute_id')
|
||||
def _check_valid_attribute(self):
|
||||
@@ -114,13 +120,14 @@ class ProductAttributeLine(models.Model):
|
||||
for product_attribute_line in self:
|
||||
product_attribute_line.product_value_ids = self.env['product.product.attribute.value'].search([
|
||||
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
|
||||
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)])
|
||||
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]
|
||||
).sorted(lambda product_product_attribute: product_product_attribute.sequence)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for product_attribute_line in self:
|
||||
self.env['product.product.attribute.value'].search([
|
||||
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
|
||||
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
|
||||
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]).unlink()
|
||||
|
||||
return super(ProductAttributeLine, self).unlink()
|
||||
@@ -152,6 +159,7 @@ class ProductAttributeLine(models.Model):
|
||||
self.env['product.product.attribute.value'].create({
|
||||
'product_attribute_value_id': product_attribute_value.id,
|
||||
'product_tmpl_id': attribute_line.product_tmpl_id.id})
|
||||
|
||||
# at this point, existing properties can be removed to reflect the modifications on value_ids
|
||||
if product_product_attribute_values_to_remove:
|
||||
product_product_attribute_values_to_remove.unlink()
|
||||
|
||||
@@ -36,13 +36,17 @@ class ProductConfiguratorController(http.Controller):
|
||||
|
||||
def _optional_product_items(self, product_id, pricelist, **kw):
|
||||
product = request.env['product.product'].with_context(self._get_product_context(pricelist, **kw)).browse(int(product_id))
|
||||
to_currency = product.currency_id
|
||||
if pricelist:
|
||||
to_currency = pricelist.currency_id
|
||||
|
||||
return request.env['ir.ui.view'].render_template("sale.optional_product_items", {
|
||||
'product': product,
|
||||
'reference_product': product,
|
||||
'pricelist': pricelist,
|
||||
'to_currency': to_currency,
|
||||
'get_attribute_value_ids': self._get_attribute_value_ids,
|
||||
'main_product_attr_ids': self._get_main_product_attr_ids(product),
|
||||
'main_product_attr_ids': self._get_main_product_attr_ids(product, pricelist=pricelist),
|
||||
})
|
||||
|
||||
def _show_optional_products(self, product_id, pricelist, handle_stock, **kw):
|
||||
@@ -74,7 +78,7 @@ class ProductConfiguratorController(http.Controller):
|
||||
'to_currency': to_currency,
|
||||
'handle_stock': handle_stock,
|
||||
'get_attribute_value_ids': self._get_attribute_value_ids,
|
||||
'main_product_attr_ids': self._get_main_product_attr_ids(product),
|
||||
'main_product_attr_ids': self._get_main_product_attr_ids(product, pricelist=pricelist),
|
||||
})
|
||||
|
||||
def _get_attribute_value_ids(self, product, reference_product=None, pricelist=None):
|
||||
@@ -111,8 +115,8 @@ class ProductConfiguratorController(http.Controller):
|
||||
|
||||
return attribute_value_ids
|
||||
|
||||
def _get_main_product_attr_ids(self, product):
|
||||
main_product_attr_ids = self._get_attribute_value_ids(product.product_tmpl_id)
|
||||
def _get_main_product_attr_ids(self, product, pricelist=None):
|
||||
main_product_attr_ids = self._get_attribute_value_ids(product.product_tmpl_id, pricelist=pricelist)
|
||||
for variant in main_product_attr_ids:
|
||||
if variant[0] == product.id:
|
||||
# We indeed need a list of lists (even with only 1 element)
|
||||
|
||||
@@ -729,7 +729,29 @@ Thanks!</field>
|
||||
<field name="optional_product_ids" eval="[(6,0,[ref('product_product_1_product_template')])]"/>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
<record id="product_attribute_value_7" model="product.attribute.value">
|
||||
<field name="name">Custom</field>
|
||||
<field name="attribute_id" ref="product.product_attribute_1"/>
|
||||
<field name="is_custom">True</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_attribute_line_1" model="product.attribute.line">
|
||||
<field name="value_ids" eval="[(4,ref('product_attribute_value_7'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_4e" model="product.product">
|
||||
<field name="default_code">DESK0005</field>
|
||||
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
|
||||
<field name="attribute_value_ids" eval="[(6,0,[ref('product_attribute_value_7'), ref('product.product_attribute_value_3')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_4f" model="product.product">
|
||||
<field name="default_code">DESK0006</field>
|
||||
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
|
||||
<field name="attribute_value_ids" eval="[(6,0,[ref('product_attribute_value_7'), ref('product.product_attribute_value_4')])]"/>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Handle automatically created product.product.attribute.value.
|
||||
Check "product.product_product_attribute_value_1" for more information about this
|
||||
-->
|
||||
@@ -759,7 +781,7 @@ Thanks!</field>
|
||||
<field name="value_ids" eval="[(6,0,[ref('sale.product_product_attribute_value_4')])]"/>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
<!--
|
||||
The "Customizable Desk's Aluminium" attribute value will excude:
|
||||
- The "Customizable Desk's Black" attribute
|
||||
- The "Office Chair's Steel" attribute
|
||||
@@ -773,7 +795,7 @@ Thanks!</field>
|
||||
<field name="value_ids" eval="[(6,0,[ref('product.product_product_attribute_value_2')])]"/>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
<!--
|
||||
The "Customizable Desk's Steel" attribute value will excude:
|
||||
- The "Office Chair's Aluminium" attribute
|
||||
-->
|
||||
|
||||
@@ -67,14 +67,25 @@ class ProductAttribute(models.Model):
|
||||
class ProductAttributeValue(models.Model):
|
||||
_inherit = "product.attribute.value"
|
||||
|
||||
is_custom = fields.Boolean('Is custom value', help="Allow users to input custom values for this attribute value")
|
||||
html_color = fields.Char(
|
||||
string='HTML Color Index', oldname='color',
|
||||
help="""Here you can set a
|
||||
specific HTML color index (e.g. #ff0000) to display the color if the
|
||||
attibute type is 'Color'.""")
|
||||
attribute type is 'Color'.""")
|
||||
|
||||
|
||||
class ProductProductAttributeValue(models.Model):
|
||||
_inherit = "product.product.attribute.value"
|
||||
|
||||
html_color = fields.Char('HTML Color Index', related="product_attribute_value_id.html_color")
|
||||
html_color = fields.Char('HTML Color Index', related="product_attribute_value_id.html_color")
|
||||
is_custom = fields.Boolean('Is custom value', related="product_attribute_value_id.is_custom")
|
||||
|
||||
|
||||
class ProductAttributeCustomValue(models.Model):
|
||||
_name = "product.attribute.custom.value"
|
||||
_rec_name = 'custom_value'
|
||||
|
||||
attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute')
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sale order line')
|
||||
custom_value = fields.Char('Custom value')
|
||||
|
||||
@@ -936,7 +936,9 @@ class SaleOrderLine(models.Model):
|
||||
res['product_id'] = int(self._context.get('product_id'))
|
||||
if self._context.get('quantity'):
|
||||
res['product_uom_qty'] = float(self._context.get('quantity'))
|
||||
|
||||
if self._context.get('product_custom_variant_values'):
|
||||
values = self._context.get('product_custom_variant_values')
|
||||
res['product_custom_variant_values'] = [(0, 0, value) for value in values]
|
||||
return res
|
||||
|
||||
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
|
||||
@@ -1126,6 +1128,7 @@ class SaleOrderLine(models.Model):
|
||||
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', readonly=True, default=True)
|
||||
product_uom_qty = fields.Float(string='Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
|
||||
product_uom = fields.Many2one('uom.uom', string='Unit of Measure')
|
||||
product_custom_variant_values = fields.One2many('product.attribute.custom.value', 'sale_order_line_id', string='Custom variant values')
|
||||
# Non-stored related field to allow portal user to see the image of the product he has ordered
|
||||
product_image = fields.Binary('Product Image', related="product_id.image", store=False)
|
||||
|
||||
@@ -1421,7 +1424,14 @@ class SaleOrderLine(models.Model):
|
||||
self.product_id = False
|
||||
return result
|
||||
|
||||
vals.update(name=self.get_sale_order_line_multiline_description_sale(product))
|
||||
name = self.get_sale_order_line_multiline_description_sale(product)
|
||||
if self.product_custom_variant_values:
|
||||
name += '\n'
|
||||
for product_custom_variant_value in self.product_custom_variant_values:
|
||||
if product_custom_variant_value.custom_value and product_custom_variant_value.custom_value.strip():
|
||||
name += '\n' + product_custom_variant_value.attribute_value_id.name + ': ' + product_custom_variant_value.custom_value.strip()
|
||||
|
||||
vals.update(name=name)
|
||||
|
||||
self._compute_tax_id()
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ access_product_product_sale_manager,product.product salemanager,model_product_pr
|
||||
access_product_attribute_sale_manager,product.attribute manager,product.model_product_attribute,sales_team.group_sale_manager,1,1,1,1
|
||||
access_product_attribute_value_sale_manager,product.attribute manager value,product.model_product_attribute_value,sales_team.group_sale_manager,1,1,1,1
|
||||
access_product_product_attribute_sale_manager,product.product.attribute manager value,product.model_product_product_attribute_value,sales_team.group_sale_manager,1,1,1,1
|
||||
access_product_product_attribute_custom_value,product.attribute.custom value manager,model_product_attribute_custom_value,sales_team.group_sale_manager,1,1,1,1
|
||||
access_product_attribute_filter_line_sale_manager,product.attribute manager filter line,product.model_product_attribute_filter_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_product_attribute_line_sale_manager,product.attribute manager line,product.model_product_attribute_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_account_tax_sale_manager,account.tax sale manager,account.model_account_tax,sales_team.group_sale_salesman,1,0,0,0
|
||||
|
||||
|
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* This adds the possibility to open the product configurator when the control
|
||||
* "Configure a product" is clicked
|
||||
*/
|
||||
odoo.define('sale.ProductConfiguratorEditableListRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
|
||||
ListRenderer.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
// we don't want the browser to navigate to a the # url
|
||||
ev.preventDefault();
|
||||
|
||||
// we don't want the click to cause other effects, such as unselecting
|
||||
// the row that we are creating, because it counts as a click on a tr
|
||||
ev.stopPropagation();
|
||||
|
||||
// but we do want to unselect current row
|
||||
var self = this;
|
||||
this.unselectRow().then(function () {
|
||||
var open_product_configurator = false;
|
||||
var context = ev.currentTarget.dataset.context;
|
||||
|
||||
var context_json = false;
|
||||
if(context){
|
||||
context = context.replace(/\'/g, '"');
|
||||
context_json = JSON.parse(context);
|
||||
|
||||
open_product_configurator = context_json.open_product_configurator;
|
||||
}
|
||||
|
||||
if(open_product_configurator){
|
||||
self._rpc({
|
||||
model: 'ir.model.data',
|
||||
method: 'xmlid_to_res_id',
|
||||
kwargs: {xmlid: 'sale.sale_product_configurator_view_form'},
|
||||
}).then(function(res_id) {
|
||||
self.do_action({
|
||||
name: _t('Configure a product'),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'sale.product.configurator',
|
||||
views: [[res_id, 'form']],
|
||||
target: 'new'
|
||||
}, {
|
||||
on_close: function (products) {
|
||||
if(products && products !== 'special'){
|
||||
self.trigger_up('add_record', {
|
||||
context: products,
|
||||
forceEditable: "bottom" ,
|
||||
allowWarning: true,
|
||||
onSuccess: function(){
|
||||
self.unselectRow();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.trigger_up('add_record', {context: context && [context]}); // TODO write a test, the deferred was not considered
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,168 +0,0 @@
|
||||
odoo.define('sale.ProductConfiguratorFormRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
var ProductConfiguratorUtils = require('sale.product_configurator_utils');
|
||||
|
||||
var ProductConfiguratorFormRenderer = FormRenderer.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$el.append('<div class="configurator_container"></div>');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Renders the product configurator within the form
|
||||
*/
|
||||
render_configurator: function (configurator) {
|
||||
var $configurator_container = this.$el.find('.configurator_container');
|
||||
$configurator_container.empty();
|
||||
|
||||
var $configurator = $(configurator);
|
||||
$configurator.appendTo($configurator_container);
|
||||
|
||||
ProductConfiguratorUtils.addConfiguratorEvents($configurator_container, "", false, $('.js_sale_order_pricelist_id').html());
|
||||
|
||||
$('.js_add_cart_variants', $configurator_container).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ProductConfiguratorFormRenderer;
|
||||
});
|
||||
|
||||
odoo.define('sale.ProductConfiguratorFormController', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var ajax = require('web.ajax');
|
||||
var FormController = require('web.FormController');
|
||||
var OptionalProductsModal = require('sale.OptionalProductsModal');
|
||||
|
||||
var ProductConfiguratorFormController = FormController.extend({
|
||||
custom_events: _.extend({}, FormController.prototype.custom_events, {
|
||||
field_changed: '_onFieldChanged'
|
||||
}),
|
||||
className: 'o_product_configurator',
|
||||
/**
|
||||
* @override
|
||||
* We need to override the default click behavior for our "Add" button
|
||||
* because there is a possibility that this product has optional products.
|
||||
* If so, we need to display an extra modal to choose the options
|
||||
*/
|
||||
_onButtonClicked: function (event) {
|
||||
if(event.stopPropagation){
|
||||
event.stopPropagation();
|
||||
}
|
||||
var attrs = event.data.attrs;
|
||||
if (attrs.special === 'cancel') {
|
||||
this._super.apply(this, arguments);
|
||||
} else {
|
||||
if(!this.$el.parents('.modal').find('.o_sale_product_configurator_add').hasClass('disabled')){
|
||||
this._handleAdd(this.$el);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* This is overriden to allow catching the "select" event on our product template select field.
|
||||
* This will not work anymore if more fields are added to the form.
|
||||
* TODO: Find a better way to catch that event.
|
||||
*/
|
||||
_onFieldChanged: function (event) {
|
||||
var self = this;
|
||||
|
||||
self.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled');
|
||||
|
||||
ajax.jsonRpc("/product_configurator/configure", 'call', {
|
||||
'product_id': event.data.changes.product_template_id.id,
|
||||
'pricelist_id': $('.js_sale_order_pricelist_id').html()
|
||||
}).then(function (configurator) {
|
||||
self.renderer.render_configurator(configurator);
|
||||
});
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* When the user adds a product that has optional products, we need to display
|
||||
* a window to allow the user to choose these extra options
|
||||
*/
|
||||
_handleAdd: function ($modal) {
|
||||
var self = this;
|
||||
var quantity = parseFloat($modal.find('input[name="add_qty"]').val() || 1);
|
||||
var product_id = parseInt($modal.find('input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked').first().val(), 10);
|
||||
|
||||
var modal = new OptionalProductsModal(quantity, product_id, $('body'), $('.js_sale_order_pricelist_id').html());
|
||||
|
||||
// No optional products found for this product,
|
||||
// only add the root product
|
||||
modal.on('options_empty', null, function () {
|
||||
self._addProducts([{
|
||||
product_id: product_id,
|
||||
quantity: quantity
|
||||
}]);
|
||||
});
|
||||
|
||||
// Add all the selected products
|
||||
modal.on('confirm', null, function () {
|
||||
self._addProducts(modal.getSelectedProducts());
|
||||
});
|
||||
|
||||
// Change the buttons translation when the modal is ready
|
||||
modal.on('modal_ready', null, function ($modal) {
|
||||
$modal.find('a.go-back').attr('title', _t('Back'));
|
||||
$modal.find('a.go-back .d-md-inline').html(_t('Back'));
|
||||
$modal.find('a.confirm').attr('title', _t('Confirm'));
|
||||
$modal.find('a.confirm .d-md-inline').html(_t('Confirm'));
|
||||
});
|
||||
|
||||
modal.appendTo($('body'));
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* This triggers the close action for the window and adds the product as the "infos" parameter.
|
||||
* It will allow the caller of this window to handle the added products.
|
||||
*/
|
||||
_addProducts: function(products) {
|
||||
this.do_action({type: 'ir.actions.act_window_close', infos: products});
|
||||
}
|
||||
});
|
||||
|
||||
return ProductConfiguratorFormController;
|
||||
});
|
||||
|
||||
odoo.define('sale.ProductConfiguratorFormView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ProductConfiguratorFormController = require('sale.ProductConfiguratorFormController');
|
||||
var ProductConfiguratorFormRenderer = require('sale.ProductConfiguratorFormRenderer');
|
||||
var FormView = require('web.FormView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var ProductConfiguratorFormView = FormView.extend({
|
||||
config: _.extend({}, FormView.prototype.config, {
|
||||
Controller: ProductConfiguratorFormController,
|
||||
Renderer: ProductConfiguratorFormRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('product_configurator_form', ProductConfiguratorFormView);
|
||||
|
||||
return ProductConfiguratorFormView;
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
odoo.define('sale.ProductConfiguratorFormController', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var FormController = require('web.FormController');
|
||||
var OptionalProductsModal = require('sale.OptionalProductsModal');
|
||||
|
||||
var ProductConfiguratorFormController = FormController.extend({
|
||||
custom_events: _.extend({}, FormController.prototype.custom_events, {
|
||||
field_changed: '_onFieldChanged'
|
||||
}),
|
||||
className: 'o_product_configurator',
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (){
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* We need to override the default click behavior for our "Add" button
|
||||
* because there is a possibility that this product has optional products.
|
||||
* If so, we need to display an extra modal to choose the options
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_onButtonClicked: function (event) {
|
||||
if (event.stopPropagation){
|
||||
event.stopPropagation();
|
||||
}
|
||||
var attrs = event.data.attrs;
|
||||
if (attrs.special === 'cancel') {
|
||||
this._super.apply(this, arguments);
|
||||
} else {
|
||||
if (!this.$el
|
||||
.parents('.modal')
|
||||
.find('.o_sale_product_configurator_add')
|
||||
.hasClass('disabled')){
|
||||
this._handleAdd(this.$el);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This is overridden to allow catching the "select" event on our product template select field.
|
||||
* This will not work anymore if more fields are added to the form.
|
||||
* TODO: Find a better way to catch that event.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_onFieldChanged: function (event) {
|
||||
var self = this;
|
||||
|
||||
this.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled');
|
||||
|
||||
this._rpc({
|
||||
route: '/product_configurator/configure',
|
||||
params: {
|
||||
product_id: event.data.changes.product_template_id.id,
|
||||
pricelist_id: $('.js_sale_order_pricelist_id').html()
|
||||
}
|
||||
}).then(function (configurator) {
|
||||
self.renderer.renderConfigurator(configurator);
|
||||
});
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* When the user adds a product that has optional products, we need to display
|
||||
* a window to allow the user to choose these extra options
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_handleAdd: function ($modal) {
|
||||
var productSelector = [
|
||||
'input[type="hidden"][name="product_id"]',
|
||||
'input[type="radio"][name="product_id"]:checked'
|
||||
];
|
||||
|
||||
this.rootProduct = {
|
||||
product_id: parseInt($modal.find(productSelector.join(', ')).first().val(), 10),
|
||||
quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1),
|
||||
product_custom_variant_values: this.renderer.getCustomVariantValues($modal.find('.js_product'))
|
||||
};
|
||||
|
||||
this.optionalProductsModal = new OptionalProductsModal($('body'), {
|
||||
rootProduct: this.rootProduct,
|
||||
pricelistId: $('.js_sale_order_pricelist_id').html(),
|
||||
okButtonText: _t('Confirm'),
|
||||
cancelButtonText: _t('Back'),
|
||||
title: _t('Configure')
|
||||
}).open();
|
||||
|
||||
this.optionalProductsModal.on('options_empty', null, this._onModalOptionsEmpty.bind(this));
|
||||
this.optionalProductsModal.on('confirm', null, this._onModalConfirm.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* No optional products found for this product, only add the root product
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onModalOptionsEmpty: function () {
|
||||
this._addProducts([this.rootProduct]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add all selected products
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onModalConfirm: function () {
|
||||
this._addProducts(this.optionalProductsModal.getSelectedProducts());
|
||||
},
|
||||
|
||||
/**
|
||||
* This triggers the close action for the window and
|
||||
* adds the product as the "infos" parameter.
|
||||
* It will allow the caller of this window to handle the added products.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} products the list of added products
|
||||
* {integer} products.product_id: the id of the product
|
||||
* {integer} products.quantity: the added quantity for this product
|
||||
*/
|
||||
_addProducts: function (products) {
|
||||
this.do_action({type: 'ir.actions.act_window_close', infos: products});
|
||||
}
|
||||
});
|
||||
|
||||
return ProductConfiguratorFormController;
|
||||
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* This adds the possibility to open the product configurator when the control
|
||||
* "Configure a product" is clicked
|
||||
*/
|
||||
odoo.define('sale.ProductConfiguratorEditableListRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
|
||||
ListRenderer.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
// we don't want the browser to navigate to a the # url
|
||||
ev.preventDefault();
|
||||
|
||||
// we don't want the click to cause other effects, such as unselecting
|
||||
// the row that we are creating, because it counts as a click on a tr
|
||||
ev.stopPropagation();
|
||||
|
||||
// but we do want to unselect current row
|
||||
var self = this;
|
||||
this.unselectRow().then(function () {
|
||||
var open_product_configurator = false;
|
||||
var context = ev.currentTarget.dataset.context;
|
||||
|
||||
var context_json = false;
|
||||
if(context){
|
||||
context = context.replace(/\'/g, '"');
|
||||
context_json = JSON.parse(context);
|
||||
|
||||
open_product_configurator = context_json.open_product_configurator;
|
||||
}
|
||||
|
||||
if(open_product_configurator){
|
||||
self._rpc({
|
||||
model: 'ir.model.data',
|
||||
method: 'xmlid_to_res_id',
|
||||
kwargs: {xmlid: 'sale.sale_product_configurator_view_form'},
|
||||
}).then(function(res_id) {
|
||||
self.do_action({
|
||||
name: _t('Configure a product'),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'sale.product.configurator',
|
||||
views: [[res_id, 'form']],
|
||||
target: 'new'
|
||||
}, {
|
||||
on_close: function (products) {
|
||||
if(products && products != 'special'){
|
||||
self.trigger_up('add_record', {
|
||||
context: products,
|
||||
forceEditable: "bottom" ,
|
||||
allowWarning: true,
|
||||
onSuccess: function(){
|
||||
self.unselectRow();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.trigger_up('add_record', {context: context && [context]}); // TODO write a test, the deferred was not considered
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,437 @@
|
||||
odoo.define('sale.ProductConfiguratorMixin', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var utils = require('web.utils');
|
||||
var ajax = require('web.ajax');
|
||||
var _t = core._t;
|
||||
|
||||
var variantsSelectors = [
|
||||
'input.js_variant_change',
|
||||
'select.js_variant_change',
|
||||
'input.js_product_change',
|
||||
'[data-attribute_value_ids]'
|
||||
];
|
||||
var variantChangeEvent = 'change ' + variantsSelectors.join(', ');
|
||||
var events = {
|
||||
'click .css_attribute_color input': '_onChangeColorAttribute',
|
||||
'change input.js_quantity': 'onChangeAddQuantity',
|
||||
'click button.js_add_cart_json': 'onClickAddCartJSON',
|
||||
};
|
||||
events[variantChangeEvent] = 'onChangeVariant';
|
||||
|
||||
var ProductConfiguratorMixin = {
|
||||
events: events,
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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 ?
|
||||
*
|
||||
*/
|
||||
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))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When a variant is changed, this will check:
|
||||
* - If the selected combination is available or not
|
||||
* - The extra price if applicable
|
||||
* - The display name of the product ("Customizable desk (White, Steel)")
|
||||
* - The new total price
|
||||
* - The need of adding a "custom value" input
|
||||
* TODO (awa): change ugly [0], [1], [2], ... to a dict
|
||||
*
|
||||
* @param {MouseEvent}
|
||||
* @param {string} params.imagesSize "small" or "medium" or ""
|
||||
*/
|
||||
onChangeVariant: function (ev, imagesSize) {
|
||||
var self = this;
|
||||
|
||||
var $parent = $(ev.target).closest('.js_product');
|
||||
var $ul = $parent.find('.js_add_cart_variants');
|
||||
var $product_id = $parent.find('.product_id').first();
|
||||
var $product_display_name = $parent.find('.product_display_name').first();
|
||||
var $product_raw_price = $parent.find('.js_raw_price').first();
|
||||
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 variant_ids = $ul.data("attribute_value_ids");
|
||||
var values = [];
|
||||
var unchanged_values = $parent
|
||||
.find('div.oe_unchanged_value_ids')
|
||||
.data('unchanged_value_ids') || [];
|
||||
|
||||
var variantsValuesSelectors = [
|
||||
'input.js_variant_change:checked',
|
||||
'select.js_variant_change'
|
||||
];
|
||||
_.each($parent.find(variantsValuesSelectors.join(', ')), function (el) {
|
||||
values.push(+$(el).val());
|
||||
});
|
||||
values = values.concat(unchanged_values);
|
||||
var list_variant_id = parseInt($parent.find('input.js_product_change:checked').val());
|
||||
|
||||
$parent.find("label").removeClass('text-muted css_not_available');
|
||||
|
||||
var product_id = false;
|
||||
var display_name = false;
|
||||
var product_raw_price = false;
|
||||
for (var k in variant_ids) {
|
||||
if (_.isEmpty(_.difference(variant_ids[k][1], values))
|
||||
|| variant_ids[k][0] === list_variant_id) {
|
||||
$price.html(self._priceToStr(variant_ids[k][2]));
|
||||
$default_price.html(self._priceToStr(variant_ids[k][3]));
|
||||
if (variant_ids[k][3]-variant_ids[k][2]>0.01) {
|
||||
$default_price
|
||||
.closest('.oe_website_sale')
|
||||
.addClass("discount");
|
||||
$optional_price
|
||||
.closest('.oe_optional')
|
||||
.show()
|
||||
.css('text-decoration', 'line-through');
|
||||
$default_price.parent().removeClass('d-none');
|
||||
} else {
|
||||
$optional_price.closest('.oe_optional').hide();
|
||||
$default_price.parent().addClass('d-none');
|
||||
}
|
||||
product_id = variant_ids[k][0];
|
||||
product_raw_price = variant_ids[k][2];
|
||||
display_name = variant_ids[k][4];
|
||||
var rootComponentSelectors = [
|
||||
'tr.js_product',
|
||||
'.oe_website_sale',
|
||||
'.o_product_configurator'
|
||||
];
|
||||
self._updateProductImage(
|
||||
$(ev.currentTarget).closest(rootComponentSelectors.join(', ')),
|
||||
product_id,
|
||||
imagesSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var variantsSelectors = [
|
||||
'input.js_variant_change:radio',
|
||||
'select.js_variant_change'
|
||||
];
|
||||
|
||||
_.each($parent.find(variantsSelectors.join(', ')), function (elem) {
|
||||
var $input = $(elem);
|
||||
var id = +$input.val();
|
||||
var values = [id];
|
||||
|
||||
variantsValuesSelectors = ['select.js_variant_change'];
|
||||
var checkedVariantSelector = "ul:not(:has(input.js_variant_change[value='" + id + "']))";
|
||||
checkedVariantSelector += " input.js_variant_change:checked";
|
||||
variantsValuesSelectors.unshift(checkedVariantSelector);
|
||||
|
||||
_.each($parent.find(variantsValuesSelectors.join(', ')), function (elem) {
|
||||
values.push(+$(elem).val());
|
||||
});
|
||||
|
||||
for (var k in variant_ids) {
|
||||
if (!_.difference(values, variant_ids[k][1]).length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$input.closest("label").addClass("css_not_available");
|
||||
$input.find("option[value='" + id + "']").addClass("css_not_available");
|
||||
});
|
||||
|
||||
if (product_id) {
|
||||
$parent.removeClass("css_not_available");
|
||||
$product_id.val(product_id);
|
||||
$product_display_name.val(display_name);
|
||||
$product_raw_price.html(product_raw_price);
|
||||
$parent
|
||||
.parents('.modal')
|
||||
.find('.o_sale_product_configurator_add')
|
||||
.removeClass("disabled");
|
||||
|
||||
var $add_to_cart_button = $parent.find("#add_to_cart");
|
||||
if (!$add_to_cart_button.hasClass('out_of_stock')) {
|
||||
$add_to_cart_button.removeClass("disabled");
|
||||
}
|
||||
} else {
|
||||
$parent.addClass("css_not_available");
|
||||
$product_id.val(0);
|
||||
$product_display_name.val('');
|
||||
$parent.find("#add_to_cart").addClass("disabled");
|
||||
$parent
|
||||
.parents('.modal')
|
||||
.find('.o_sale_product_configurator_add')
|
||||
.addClass("disabled");
|
||||
}
|
||||
|
||||
this.computePriceTotal();
|
||||
this.handleCustomValues(ev);
|
||||
},
|
||||
|
||||
/**
|
||||
* Will add the "custom value" input for this attribute value if necessary
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
handleCustomValues: function (ev){
|
||||
var $variantContainer;
|
||||
var addCustomValueField = false;
|
||||
var attributeValueId = false;
|
||||
var attributeValueName = false;
|
||||
if ($(ev.target).is('input[type=radio]') && $(ev.target).is(':checked')) {
|
||||
$variantContainer = $(ev.target).closest('ul').closest('li');
|
||||
addCustomValueField = $(ev.target).data('is_custom');
|
||||
attributeValueId = $(ev.target).data('value_id');
|
||||
attributeValueName = $(ev.target).data('value_name');
|
||||
} else if ($(ev.target).is('select')){
|
||||
$variantContainer = $(ev.target).closest('li');
|
||||
var $selectedOption = $(ev.target)
|
||||
.find('option[value="' + $(ev.target).val() + '"]');
|
||||
addCustomValueField = $selectedOption.data('is_custom');
|
||||
attributeValueId = $selectedOption.data('value_id');
|
||||
attributeValueName = $selectedOption.data('value_name');
|
||||
}
|
||||
|
||||
if ($variantContainer) {
|
||||
if (addCustomValueField && attributeValueId && attributeValueName) {
|
||||
if ($variantContainer.find('.variant_custom_value').length === 0
|
||||
|| $variantContainer
|
||||
.find('.variant_custom_value')
|
||||
.data('attribute_value_id') !== parseInt(attributeValueId)){
|
||||
$variantContainer.find('.variant_custom_value_label').remove();
|
||||
$variantContainer.find('.variant_custom_value').remove();
|
||||
|
||||
var $label = $('<label>', {
|
||||
html: attributeValueName + ': ',
|
||||
class: 'variant_custom_value_label'
|
||||
});
|
||||
|
||||
var $input = $('<input>', {
|
||||
type: 'text',
|
||||
'data-attribute_value_id': attributeValueId,
|
||||
'data-attribute_value_name': attributeValueName,
|
||||
class: 'variant_custom_value form-control'
|
||||
});
|
||||
$variantContainer.append($label).append($input);
|
||||
}
|
||||
} else {
|
||||
$variantContainer.find('.variant_custom_value_label').remove();
|
||||
$variantContainer.find('.variant_custom_value').remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hack to add and remove from cart with json
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickAddCartJSON: function (ev) {
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
var $input = $link.closest('.input-group').find("input");
|
||||
var product_id = +$input
|
||||
.closest('*:has(input[name="product_id"])')
|
||||
.find('input[name="product_id"]')
|
||||
.val();
|
||||
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 new_qty = quantity > min ? (quantity < max ? quantity : max) : min;
|
||||
// if they are more of one input for this product (eg: option modal)
|
||||
$('input[name="'+$input.attr("name")+'"]').add($input).filter(function () {
|
||||
var $prod = $(this).closest('*:has(input[name="product_id"])');
|
||||
return !$prod.length || +$prod.find('input[name="product_id"]').val() === product_id;
|
||||
}).val(new_qty).trigger('change', [false]);
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* When the quantity is changed, we need to query the new price of the product.
|
||||
* Based on the price list, the price might change when quantity exceeds X
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onChangeAddQuantity: function (ev, noStockCheck) {
|
||||
var self = this;
|
||||
var $parent;
|
||||
if ($(ev.currentTarget).closest('form').length > 0){
|
||||
$parent = $(ev.currentTarget).closest('form');
|
||||
} else if ($(ev.currentTarget).closest('.oe_optional_products_modal').length > 0){
|
||||
$parent = $(ev.currentTarget).closest('.oe_optional_products_modal');
|
||||
} else {
|
||||
$parent = $(ev.currentTarget).closest('.o_product_configurator');
|
||||
}
|
||||
|
||||
var qty = $parent.find('input[name="add_qty"]').val();
|
||||
var productIDs = [];
|
||||
$parent.find(".js_product .js_add_cart_variants").each(function () {
|
||||
_.each($(this).data("attribute_value_ids"), function (entry){
|
||||
productIDs.push(entry[0]);
|
||||
});
|
||||
});
|
||||
|
||||
var getPriceUrl = '/product_configurator/get_unit_price' + (this.isWebsite ? '_website' : '');
|
||||
if (productIDs.length) {
|
||||
ajax.jsonRpc(getPriceUrl, 'call', {
|
||||
product_ids: productIDs,
|
||||
add_qty: parseInt(qty),
|
||||
pricelist_id: this.pricelistId
|
||||
}).then(function (data) {
|
||||
$parent.find(".js_product .js_add_cart_variants").each(function () {
|
||||
var currentAttributesData = $(this).data("attribute_value_ids");
|
||||
for (var j = 0 ; j < currentAttributesData.length ; j++) {
|
||||
// update the price of the product (index "2") based on its id (index "0")
|
||||
currentAttributesData[j][2] = data[currentAttributesData[j][0]];
|
||||
}
|
||||
$(this).trigger('change', [noStockCheck]);
|
||||
|
||||
self.computePriceTotal();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers the price computation and other variant specific changes
|
||||
*
|
||||
* @param {$.Element} $container
|
||||
*/
|
||||
triggerVariantChange: function ($container) {
|
||||
$container.find('.js_add_cart_variants li[data-attribute_id]').each(function () {
|
||||
$(this)
|
||||
.find('input.js_variant_change, select.js_variant_change')
|
||||
.first()
|
||||
.trigger('change');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Will look for user custom attribute values
|
||||
* in the provided container
|
||||
*
|
||||
* @param {$.Element} $container
|
||||
* @returns {Array} array of custom values with the following format
|
||||
* {integer} attribute_value_id
|
||||
* {string} attribute_value_name
|
||||
* {string} custom_value
|
||||
*/
|
||||
getCustomVariantValues: function ($container) {
|
||||
var variantCustomValues = [];
|
||||
$container.find('.variant_custom_value').each(function (){
|
||||
var $variantCustomValueInput = $(this);
|
||||
if ($variantCustomValueInput.length !== 0){
|
||||
variantCustomValues.push({
|
||||
'attribute_value_id': $variantCustomValueInput.data('attribute_value_id'),
|
||||
'attribute_value_name': $variantCustomValueInput.data('attribute_value_name'),
|
||||
'custom_value': $variantCustomValueInput.val(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return variantCustomValues;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* returns the formatted price
|
||||
*
|
||||
* @private
|
||||
* @param {float} price
|
||||
*/
|
||||
_priceToStr: function (price) {
|
||||
var l10n = _t.database.parameters;
|
||||
var precision = 2;
|
||||
|
||||
if ($('.decimal_precision').length) {
|
||||
precision = parseInt($('.decimal_precision').last().data('precision'));
|
||||
}
|
||||
var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.');
|
||||
formatted[0] = utils.insert_thousand_seps(formatted[0]);
|
||||
return formatted.join(l10n.decimal_point);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the product image
|
||||
*
|
||||
* @private
|
||||
* @param {$.Element} $productContainer
|
||||
* @param {integer} product_id
|
||||
* @param {string} imagesSize
|
||||
*/
|
||||
_updateProductImage: function ($productContainer, product_id, imagesSize) {
|
||||
var $img;
|
||||
var imageSrc = '/web/image?model=product.product&id={0}&field={1}'
|
||||
.replace("{0}", product_id)
|
||||
.replace("{1}", _.isEmpty(imagesSize) ? 'image' : ('image_' + imagesSize));
|
||||
|
||||
if ($productContainer.find('#o-carousel-product').length) {
|
||||
$img = $productContainer.find('img.js_variant_img');
|
||||
$img.attr("src", imageSrc);
|
||||
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
|
||||
.data('oe-model', 'product.product').data('oe-id', product_id);
|
||||
|
||||
var $thumbnail = $productContainer.find('img.js_variant_img_small');
|
||||
if ($thumbnail.length !== 0) { // if only one, thumbnails are not displayed
|
||||
$thumbnail.attr("src", "/web/image/product.product/" + product_id + "/image/90x90");
|
||||
$('.carousel').carousel(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var imagesSelectors = [
|
||||
'span[data-oe-model^="product."][data-oe-type="image"] img:first',
|
||||
'img.product_detail_img',
|
||||
'span.variant_image img'
|
||||
];
|
||||
|
||||
$img = $productContainer.find(imagesSelectors.join(', '));
|
||||
$img.attr('src', imageSrc);
|
||||
$img.parent()
|
||||
.attr('data-oe-model', 'product.product')
|
||||
.attr('data-oe-id', product_id)
|
||||
.data('oe-model', 'product.product')
|
||||
.data('oe-id', product_id);
|
||||
}
|
||||
// reset zooming constructs
|
||||
$img.filter('[data-zoom-image]').attr('data-zoom-image', $img.attr('src'));
|
||||
if ($img.data('zoomOdoo') !== undefined) {
|
||||
$img.data('zoomOdoo').isReady = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight selected color
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onChangeColorAttribute: function (ev) {
|
||||
var $parent = $(ev.target).closest('.js_product');
|
||||
$parent.find('.css_attribute_color')
|
||||
.removeClass("active")
|
||||
.filter(':has(input:checked)')
|
||||
.addClass("active");
|
||||
}
|
||||
};
|
||||
|
||||
return ProductConfiguratorMixin;
|
||||
|
||||
});
|
||||
@@ -2,28 +2,45 @@ odoo.define('sale.OptionalProductsModal', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var Widget = require('web.Widget');
|
||||
var Dialog = require('web.Dialog');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
|
||||
var weContext = require('web_editor.context');
|
||||
var ProductConfiguratorUtils = require('sale.product_configurator_utils');
|
||||
|
||||
var product_name_map = {};
|
||||
var optional_products_map = {};
|
||||
|
||||
var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
events: _.extend({}, Widget.prototype.events, {
|
||||
var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixin, {
|
||||
events: _.extend({}, Dialog.prototype.events, ProductConfiguratorMixin.events, {
|
||||
'click a.js_add, a.js_remove': '_onAddOrRemoveOption',
|
||||
'change input[name="add_qty"]': '_onChangeMainProductQuantity',
|
||||
'click .css_attribute_color input': '_onColorClick',
|
||||
'change input.js_quantity': '_onChangeQuantity'
|
||||
}),
|
||||
/**
|
||||
* Initializes the optional products modal
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function (quantity, product_id, container, pricelistId, isWebsite) {
|
||||
this._super.apply(this, arguments);
|
||||
this.quantity = quantity;
|
||||
this.product_id = product_id;
|
||||
this.container = container;
|
||||
this.pricelistId = pricelistId;
|
||||
this.isWebsite = isWebsite;
|
||||
init: function (parent, params) {
|
||||
this._super(parent, {
|
||||
size: 'large',
|
||||
buttons: [{
|
||||
text: params.okButtonText,
|
||||
click: this._onConfirmButtonClick,
|
||||
classes: 'btn-primary'
|
||||
}, {
|
||||
text: params.cancelButtonText,
|
||||
click: this._onCancelButtonClick
|
||||
}],
|
||||
title: params.title
|
||||
});
|
||||
|
||||
this.rootProduct = params.rootProduct;
|
||||
this.container = parent;
|
||||
this.pricelistId = params.pricelistId;
|
||||
this.isWebsite = params.isWebsite;
|
||||
this.dialogClass = 'oe_optional_products_modal' + (params.isWebsite ? ' oe_website_sale' : '');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -31,26 +48,69 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
|
||||
var getModalContent = ajax.jsonRpc(this._getUri("/product_configurator/show_optional_products"), 'call', {
|
||||
product_id: self.product_id,
|
||||
var uri = this._getUri("/product_configurator/show_optional_products");
|
||||
var getModalContent = ajax.jsonRpc(uri, 'call', {
|
||||
product_id: self.rootProduct.product_id,
|
||||
pricelist_id: self.pricelistId,
|
||||
kwargs: {
|
||||
context: _.extend({'quantity': self.quantity}, weContext.get()),
|
||||
context: _.extend({'quantity': self.rootProduct.quantity}, weContext.get()),
|
||||
}
|
||||
})
|
||||
.then(function (modal_content) {
|
||||
self.modal_content = modal_content;
|
||||
.then(function (modalContent) {
|
||||
if (modalContent){
|
||||
var $modalContent = $(modalContent);
|
||||
$modalContent = self._postProcessContent($modalContent);
|
||||
self.$content = $modalContent;
|
||||
} else {
|
||||
self.trigger('options_empty');
|
||||
self.preventOpening = true;
|
||||
}
|
||||
});
|
||||
|
||||
var parentInit = self._super.apply(self, arguments);
|
||||
return $.when(getModalContent, parentInit);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a dialog
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.shouldFocusButtons if true, put the focus on
|
||||
* the first button primary when the dialog opens
|
||||
*/
|
||||
open: function (options) {
|
||||
$('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened
|
||||
|
||||
var self = this;
|
||||
this.appendTo($('<div/>')).then(function () {
|
||||
if (!self.preventOpening){
|
||||
self.$modal.find(".modal-body").replaceWith(self.$el);
|
||||
self.$modal.attr('open', true);
|
||||
self.$modal.removeAttr("aria-hidden");
|
||||
self.$modal.modal().appendTo(self.container);
|
||||
self._opened.resolve();
|
||||
}
|
||||
});
|
||||
if (options && options.shouldFocusButtons) {
|
||||
self._onFocusControlButton();
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
/**
|
||||
* Will:
|
||||
* - trigger add_qty change to synchronize quantity with previous window
|
||||
* - trigger variant change to compute the price and other
|
||||
* variant specific changes
|
||||
*
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function() {
|
||||
this._renderModal();
|
||||
return this._super.apply(this, arguments);
|
||||
start: function (){
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.$el.find('input[name="add_qty"]').val(this.rootProduct.quantity).change();
|
||||
this.triggerVariantChange(this.$el);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -58,24 +118,32 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Returns the list of selected products as products[{product_id, quantity}]
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getSelectedProducts: function() {
|
||||
getSelectedProducts: function () {
|
||||
var self = this;
|
||||
var products = [];
|
||||
this.$modal.find('.js_product.in_cart').each(function(){
|
||||
var products = [this.rootProduct];
|
||||
this.$modal.find('.js_product.in_cart:not(.main_product)').each(function (){
|
||||
var quantity = 0;
|
||||
var $item = $(this);
|
||||
if($item.find('input[name="add_qty"]').length){
|
||||
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());
|
||||
quantity = parseInt(
|
||||
$item
|
||||
.find('.optional_product_quantity span.add_qty')
|
||||
.html()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var productCustomVariantValues = self.getCustomVariantValues($(this));
|
||||
products.push({
|
||||
product_id: $item.find('input.product_id').length ? $item.find('input.product_id').val() : self.product_id,
|
||||
quantity: quantity
|
||||
product_id: parseInt($item.find('input.product_id').val()),
|
||||
quantity: quantity,
|
||||
product_custom_variant_values: productCustomVariantValues
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,34 +154,40 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
// Private
|
||||
// ------------------------------------------
|
||||
|
||||
_renderModal: function(){
|
||||
var self = this;
|
||||
if (self.modal_content){
|
||||
var $modal = $(self.modal_content);
|
||||
$modal.find('img:first').attr("src", "/web/image/product.product/" + self.product_id + "/image_medium");
|
||||
|
||||
$modal
|
||||
.modal()
|
||||
.appendTo(self.container)
|
||||
.addClass('oe_optional_products_modal' + (self.isWebsite ? ' oe_website_sale' : ''))
|
||||
.on('hidden.bs.modal', function () {
|
||||
$(this).remove();
|
||||
})
|
||||
.on('shown.bs.modal', function () {
|
||||
self.trigger('modal_ready', $modal);
|
||||
});
|
||||
/**
|
||||
* Adds the product image and updates the product description
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_postProcessContent: function ($modalContent) {
|
||||
var productId = this.rootProduct.product_id;
|
||||
$modalContent
|
||||
.find('img:first')
|
||||
.attr("src", "/web/image/product.product/" + productId + "/image_medium");
|
||||
|
||||
self.$modal = $modal;
|
||||
self._addModalEvents();
|
||||
} else {
|
||||
self.trigger('options_empty');
|
||||
if (this.rootProduct && this.rootProduct.product_custom_variant_values) {
|
||||
var $productDescription = $modalContent
|
||||
.find('.main_product')
|
||||
.find('td.td-product_name div.text-muted.small');
|
||||
var description = $productDescription.html();
|
||||
$.each(this.rootProduct.product_custom_variant_values, function (){
|
||||
description += '<br/>' + this.attribute_value_name + ': ' + this.custom_value;
|
||||
});
|
||||
|
||||
$productDescription.html(description);
|
||||
}
|
||||
|
||||
return $modalContent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Website behavior is slighlty different from backend so we append "_website" to URLs to lead to a different route
|
||||
* Website behavior is slightly different from backend so we append
|
||||
* "_website" to URLs to lead to a different route
|
||||
*
|
||||
* @private
|
||||
* @param {string} uri The uri to adapt
|
||||
*/
|
||||
_getUri: function(uri) {
|
||||
_getUri: function (uri) {
|
||||
if (this.isWebsite){
|
||||
return uri + '_website';
|
||||
} else {
|
||||
@@ -121,56 +195,49 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
_addModalEvents: function() {
|
||||
this.$modal.on('click', '.a-submit', {self: this}, this._onSubmit);
|
||||
this.$modal.on('click', '.css_attribute_color input', this._onColorClick);
|
||||
this.$modal.on("click", 'a.js_add, a.js_remove', {self: this}, this._onAddOrRemoveOption);
|
||||
this.$modal.on("change", "input.js_quantity", {self: this}, this._onChangeQuantity);
|
||||
this.$modal.on("change", 'input[name="add_qty"]', this._onChangeMainProductQuantity);
|
||||
ProductConfiguratorUtils.addConfiguratorEvents(this.$modal, "small", this.isWebsite, this.pricelistId);
|
||||
|
||||
// trigger add_qty change to synchronise quantity with previous window
|
||||
this.$modal.find('input[name="add_qty"]').val(this.quantity).change();
|
||||
|
||||
// trigger input change to validate fields and display custom prices
|
||||
$('.js_add_cart_variants', this.$modal).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
_onConfirmButtonClick: function () {
|
||||
this.trigger('confirm');
|
||||
this.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the "Back" and "Confirm" buttons and triggers the according events
|
||||
*/
|
||||
_onSubmit: function(ev){
|
||||
var $modal = $(this).parents('.oe_optional_products_modal');
|
||||
$modal.modal('hide');
|
||||
ev.preventDefault();
|
||||
|
||||
var $a = $(this);
|
||||
ev.data.self.trigger($a.hasClass('confirm') ? 'confirm' : 'back');
|
||||
_onCancelButtonClick: function () {
|
||||
this.trigger('back');
|
||||
this.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* TODO (awa): check if necessary
|
||||
*/
|
||||
_onColorClick: function(){
|
||||
var $modal = $(this).parents('.oe_optional_products_modal');
|
||||
_onColorClick: function (ev){
|
||||
var $modal = $(ev.currentTarget).parents('.oe_optional_products_modal');
|
||||
$modal.find('.css_attribute_color').removeClass("active");
|
||||
$modal.find('.css_attribute_color:has(input:checked)').addClass("active");
|
||||
|
||||
this._onChangeColorAttribute(ev);
|
||||
},
|
||||
|
||||
_onAddOrRemoveOption: function(ev){
|
||||
/**
|
||||
* Will add/remove the option, that includes:
|
||||
* - Moving it to the correct DOM section
|
||||
* and possibly under its parent product
|
||||
* - Hiding variants and showing the quantity
|
||||
* - Remove optional products if parent product is removed
|
||||
* - Compute the total price
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddOrRemoveOption: function (ev){
|
||||
ev.preventDefault();
|
||||
var $modal = $(this).parents('.oe_optional_products_modal');
|
||||
var self = this;
|
||||
var $target = $(ev.currentTarget);
|
||||
var $modal = $target.parents('.oe_optional_products_modal');
|
||||
var $select_options_text = $modal.find('.o_select_options');
|
||||
var $main_product = $modal.find('.js_product:first');
|
||||
var $parent = $(this).parents('.js_product:first');
|
||||
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( $(this).hasClass("js_add") ? 1 : 0 );
|
||||
$parent.find("input.js_optional_same_quantity").val($target.hasClass("js_add") ? 1 : 0 );
|
||||
$parent.find(".js_remove");
|
||||
|
||||
var product_id = $parent.find(".product_template_id").val();
|
||||
if ($(this).hasClass('js_add')) {
|
||||
if ($target.hasClass('js_add')) {
|
||||
// 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');
|
||||
@@ -181,11 +248,29 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
product_name_map[product_id] = $parent.find(".product-name").html();
|
||||
$parent.find(".product-name").html($parent.find(".product_display_name").val());
|
||||
|
||||
var product_custom_variant_values = self.getCustomVariantValues($parent);
|
||||
if (product_custom_variant_values) {
|
||||
var $productDescription = $parent
|
||||
.find('td.td-product_name div.float-left');
|
||||
|
||||
var description = '';
|
||||
$.each(product_custom_variant_values, function (){
|
||||
description += '<br/>' + this.attribute_value_name + ': ' + this.custom_value;
|
||||
});
|
||||
|
||||
var $customAttributeValuesDescription = $('<div>', {
|
||||
class: 'custom_attribute_values_description text-muted small',
|
||||
html: description
|
||||
});
|
||||
|
||||
$productDescription.append($customAttributeValuesDescription);
|
||||
}
|
||||
|
||||
// if it's an optional product of an optional product, place it after it's parent
|
||||
var parent_product_id = null;
|
||||
for (var produt_id_key in optional_products_map) {
|
||||
if (optional_products_map[produt_id_key].indexOf(product_id) != -1) {
|
||||
parent_product_id = produt_id_key;
|
||||
for (var product_id_key in optional_products_map) {
|
||||
if (optional_products_map[product_id_key].indexOf(product_id) !== -1) {
|
||||
parent_product_id = product_id_key;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -201,18 +286,14 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
$main_product.after($parent);
|
||||
}
|
||||
|
||||
var isWebsite = ev.data.self.isWebsite;
|
||||
var pricelistId = ev.data.self.pricelistId;
|
||||
var product_variant_id = $parent.find('.product_id').val();
|
||||
ajax.jsonRpc(ev.data.self._getUri("/product_configurator/optional_product_items"), 'call', {
|
||||
ajax.jsonRpc(self._getUri("/product_configurator/optional_product_items"), 'call', {
|
||||
'product_id': product_variant_id,
|
||||
'pricelist_id': ev.data.self.pricelistId
|
||||
'pricelist_id': self.pricelistId
|
||||
}).then(function (addedItem) {
|
||||
var $addedItem = $(addedItem);
|
||||
$modal.find('tr:last').after($addedItem);
|
||||
|
||||
ProductConfiguratorUtils.addConfiguratorEvents($addedItem, "small", isWebsite, pricelistId);
|
||||
|
||||
// trigger input change to validate fields
|
||||
$('.js_add_cart_variants', $addedItem).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
@@ -229,8 +310,8 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
}
|
||||
|
||||
if($select_options_text.nextAll('.js_product').length === 0){
|
||||
|
||||
if ($select_options_text.nextAll('.js_product').length === 0){
|
||||
// no more optional products to select -> hide the header
|
||||
$select_options_text.hide();
|
||||
}
|
||||
@@ -240,6 +321,7 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
$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('.custom_attribute_values_description').remove();
|
||||
|
||||
$select_options_text.show();
|
||||
|
||||
@@ -260,12 +342,15 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
$('tr:last').after($parent);
|
||||
}
|
||||
|
||||
ProductConfiguratorUtils.computePriceTotal();
|
||||
self.computePriceTotal();
|
||||
},
|
||||
|
||||
_onChangeQuantity: function(ev){
|
||||
var qty = parseFloat($(this).val());
|
||||
var $modal = $(this).parents('.oe_optional_products_modal');
|
||||
_onChangeQuantity: function (ev){
|
||||
this.onChangeAddQuantity(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');
|
||||
@@ -275,8 +360,9 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeMainProductQuantity: function(){
|
||||
var $modal = $(this).parents('.oe_optional_products_modal');
|
||||
_onChangeMainProductQuantity: function (ev){
|
||||
var $quantity = $(ev.currentTarget);
|
||||
var $modal = $quantity.parents('.oe_optional_products_modal');
|
||||
var product_id = $modal.find('span.oe_price[data-product-id]').first().data('product-id');
|
||||
var product_ids = [product_id];
|
||||
var $products_dom = [];
|
||||
@@ -288,11 +374,10 @@ var OptionalProductsModal = Widget.extend(ServicesMixin, {
|
||||
});
|
||||
});
|
||||
|
||||
// trigger input change to validate fields and display custom prices
|
||||
$('.js_add_cart_variants', $modal).each(function () {
|
||||
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
|
||||
});
|
||||
this.triggerVariantChange($modal);
|
||||
}
|
||||
});
|
||||
|
||||
return OptionalProductsModal;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
odoo.define('sale.ProductConfiguratorFormRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
|
||||
|
||||
var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMixin ,{
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (){
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.pricelistId = $('.js_sale_order_pricelist_id').html();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.$el.append($('<div>', {class: 'configurator_container'}));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Renders the product configurator within the form
|
||||
*
|
||||
* Will also:
|
||||
* - add events handling for variant changes
|
||||
* - trigger variant change to compute the price and other
|
||||
* variant specific changes
|
||||
*
|
||||
* @param {string} configuratorHtml the evaluated template of
|
||||
* the product configurator
|
||||
*/
|
||||
renderConfigurator: function (configuratorHtml) {
|
||||
var $configuratorContainer = this.$('.configurator_container');
|
||||
$configuratorContainer.empty();
|
||||
|
||||
var $configuratorHtml = $(configuratorHtml);
|
||||
$configuratorHtml.appendTo($configuratorContainer);
|
||||
|
||||
this.triggerVariantChange($configuratorContainer);
|
||||
}
|
||||
});
|
||||
|
||||
return ProductConfiguratorFormRenderer;
|
||||
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
odoo.define('sale.product_configurator_utils', function (require) {
|
||||
'use strict';
|
||||
var core = require('web.core');
|
||||
var utils = require('web.utils');
|
||||
var ajax = require('web.ajax');
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* returns the formatted price
|
||||
* @param {float} price
|
||||
*/
|
||||
function _priceToStr (price) {
|
||||
var l10n = _t.database.parameters;
|
||||
var precision = 2;
|
||||
|
||||
if ($(".decimal_precision").length) {
|
||||
precision = parseInt($(".decimal_precision").last().data('precision'));
|
||||
}
|
||||
var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.');
|
||||
formatted[0] = utils.insert_thousand_seps(formatted[0]);
|
||||
return formatted.join(l10n.decimal_point);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ?
|
||||
*/
|
||||
function computePriceTotal (){
|
||||
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(_priceToStr(parseFloat(price)));
|
||||
}
|
||||
}
|
||||
|
||||
function _updateProductImage ($productContainer, product_id, imagesSize) {
|
||||
var $img;
|
||||
var imageSrc = '/web/image?model=product.product&id={0}&field={1}'
|
||||
.replace("{0}", product_id)
|
||||
.replace("{1}", imagesSize ? ('image_' + imagesSize) : 'image');
|
||||
if ($productContainer.find('#o-carousel-product').length) {
|
||||
$img = $productContainer.find('img.js_variant_img');
|
||||
$img.attr("src", imageSrc);
|
||||
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
|
||||
.data('oe-model', 'product.product').data('oe-id', product_id);
|
||||
|
||||
var $thumbnail = $productContainer.find('img.js_variant_img_small');
|
||||
if ($thumbnail.length !== 0) { // if only one, thumbnails are not displayed
|
||||
$thumbnail.attr("src", "/web/image/product.product/" + product_id + "/image/90x90");
|
||||
$('.carousel').carousel(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$img = $productContainer.find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img, span.variant_image img');
|
||||
$img.attr("src", imageSrc);
|
||||
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
|
||||
.data('oe-model', 'product.product').data('oe-id', product_id);
|
||||
}
|
||||
// reset zooming constructs
|
||||
$img.filter('[data-zoom-image]').attr('data-zoom-image', $img.attr('src'));
|
||||
if ($img.data('zoomOdoo') !== undefined) {
|
||||
$img.data('zoomOdoo').isReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a variant is changed, this will compute:
|
||||
* - If the selected combination is available or not
|
||||
* - The extra price if applicable
|
||||
* - The display name of the product ("Customizable desk (White, Steel)")
|
||||
* TODO (awa): change ugly [0], [1], [2], ... to a dict
|
||||
*/
|
||||
function onChangeVariant (ev, imagesSize) {
|
||||
var $parent = $(ev.target).closest('.js_product');
|
||||
var $ul = $parent.find('.js_add_cart_variants');
|
||||
var $product_id = $parent.find('.product_id').first();
|
||||
var $product_display_name = $parent.find('.product_display_name').first();
|
||||
var $product_raw_price = $parent.find('.js_raw_price').first();
|
||||
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 variant_ids = $ul.data("attribute_value_ids");
|
||||
var values = [];
|
||||
var unchanged_values = $parent.find('div.oe_unchanged_value_ids').data('unchanged_value_ids') || [];
|
||||
|
||||
_.each($parent.find('input.js_variant_change:checked, select.js_variant_change'), function (el) {
|
||||
values.push(+$(el).val());
|
||||
});
|
||||
values = values.concat(unchanged_values);
|
||||
var list_variant_id = parseInt($parent.find('input.js_product_change:checked').val());
|
||||
|
||||
$parent.find("label").removeClass("text-muted css_not_available");
|
||||
|
||||
var product_id = false;
|
||||
var display_name = false;
|
||||
var product_raw_price = false;
|
||||
for (var k in variant_ids) {
|
||||
if (_.isEmpty(_.difference(variant_ids[k][1], values)) || variant_ids[k][0] === list_variant_id) {
|
||||
$price.html(_priceToStr(variant_ids[k][2]));
|
||||
$default_price.html(_priceToStr(variant_ids[k][3]));
|
||||
if (variant_ids[k][3]-variant_ids[k][2]>0.01) {
|
||||
$default_price.closest('.oe_website_sale').addClass("discount");
|
||||
$optional_price.closest('.oe_optional').show().css('text-decoration', 'line-through');
|
||||
$default_price.parent().removeClass('d-none');
|
||||
} else {
|
||||
$optional_price.closest('.oe_optional').hide();
|
||||
$default_price.parent().addClass('d-none');
|
||||
}
|
||||
product_id = variant_ids[k][0];
|
||||
product_raw_price = variant_ids[k][2];
|
||||
display_name = variant_ids[k][4];
|
||||
_updateProductImage($(ev.currentTarget).closest('tr.js_product, .oe_website_sale, .o_product_configurator'), product_id, imagesSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_.each($parent.find("input.js_variant_change:radio, select.js_variant_change"), function (elem) {
|
||||
var $input = $(elem);
|
||||
var id = +$input.val();
|
||||
var values = [id];
|
||||
|
||||
_.each($parent.find("ul:not(:has(input.js_variant_change[value='" + id + "'])) input.js_variant_change:checked, select.js_variant_change"), function (elem) {
|
||||
values.push(+$(elem).val());
|
||||
});
|
||||
|
||||
for (var k in variant_ids) {
|
||||
if (!_.difference(values, variant_ids[k][1]).length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$input.closest("label").addClass("css_not_available");
|
||||
$input.find("option[value='" + id + "']").addClass("css_not_available");
|
||||
});
|
||||
|
||||
if (product_id) {
|
||||
$parent.removeClass("css_not_available");
|
||||
$product_id.val(product_id);
|
||||
$product_display_name.val(display_name);
|
||||
$product_raw_price.html(product_raw_price);
|
||||
$parent.parents('.modal').find('.o_sale_product_configurator_add').removeClass("disabled");
|
||||
|
||||
var $add_to_cart_button = $parent.find("#add_to_cart");
|
||||
if(!$add_to_cart_button.hasClass('out_of_stock')){
|
||||
$add_to_cart_button.removeClass("disabled");
|
||||
}
|
||||
} else {
|
||||
$parent.addClass("css_not_available");
|
||||
$product_id.val(0);
|
||||
$product_display_name.val('');
|
||||
$parent.find("#add_to_cart").addClass("disabled");
|
||||
$parent.parents('.modal').find('.o_sale_product_configurator_add').addClass("disabled");
|
||||
}
|
||||
|
||||
computePriceTotal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight selected color
|
||||
*/
|
||||
function _onChangeColorAttribute (ev) {
|
||||
var $parent = $(ev.target).closest('.js_product');
|
||||
$parent.find('.css_attribute_color')
|
||||
.removeClass("active")
|
||||
.filter(':has(input:checked)')
|
||||
.addClass("active");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hack to add and remove from cart with json
|
||||
*/
|
||||
function onClickAddCartJSON (ev) {
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
var $input = $link.closest('.input-group').find("input");
|
||||
var product_id = +$input.closest('*:has(input[name="product_id"])').find('input[name="product_id"]').val();
|
||||
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 new_qty = quantity > min ? (quantity < max ? quantity : max) : min;
|
||||
// if they are more of one input for this product (eg: option modal)
|
||||
$('input[name="'+$input.attr("name")+'"]').add($input).filter(function () {
|
||||
var $prod = $(this).closest('*:has(input[name="product_id"])');
|
||||
return !$prod.length || +$prod.find('input[name="product_id"]').val() === product_id;
|
||||
}).val(new_qty).change();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the quantity is changed, we need to query the new price of the product.
|
||||
* Based on the pricelist, the price might change when quantity exceeds X
|
||||
*/
|
||||
function onChangeAddQuantity (ev, isWebsite, pricelistId, no_stock_check) {
|
||||
var $parent;
|
||||
if($(ev.currentTarget).closest('form').length > 0){
|
||||
$parent = $(ev.currentTarget).closest('form');
|
||||
} else if($(ev.currentTarget).closest('.oe_optional_products_modal').length > 0){
|
||||
$parent = $(ev.currentTarget).closest('.oe_optional_products_modal');
|
||||
} else {
|
||||
$parent = $(ev.currentTarget).closest('.o_product_configurator');
|
||||
}
|
||||
|
||||
var qty = $parent.find('input[name="add_qty"]').val();
|
||||
var productIDs = [];
|
||||
$parent.find(".js_product .js_add_cart_variants").each(function () {
|
||||
_.each($(this).data("attribute_value_ids"), function(entry){
|
||||
productIDs.push(entry[0]);
|
||||
});
|
||||
});
|
||||
|
||||
if (productIDs.length) {
|
||||
ajax.jsonRpc('/product_configurator/get_unit_price' + (isWebsite ? '_website' : ''), 'call', {
|
||||
product_ids: productIDs,
|
||||
add_qty: parseInt(qty),
|
||||
pricelist_id: pricelistId
|
||||
}).then(function (data) {
|
||||
$parent.find(".js_product .js_add_cart_variants").each(function () {
|
||||
var currentAttributesData = $(this).data("attribute_value_ids");
|
||||
for (var j = 0 ; j < currentAttributesData.length ; j++) {
|
||||
// update the price of the product (index "2") based on its id (index "0")
|
||||
currentAttributesData[j][2] = data[currentAttributesData[j][0]];
|
||||
}
|
||||
$(this).trigger('change', [no_stock_check]);
|
||||
|
||||
computePriceTotal();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all the necessary events to handle:
|
||||
* - Product variant changes
|
||||
* - Color changes
|
||||
* - Product price based on pricelist and quantity
|
||||
* - Total price
|
||||
* ...
|
||||
*/
|
||||
function addConfiguratorEvents ($container, imagesSize, isWebsite, pricelistId) {
|
||||
$container
|
||||
.find('.css_attribute_color input')
|
||||
.change(_onChangeColorAttribute);
|
||||
|
||||
$container
|
||||
.find('input.js_variant_change, select.js_variant_change, input.js_product_change, [data-attribute_value_ids]')
|
||||
.change(function(ev, no_stock_check){
|
||||
onChangeVariant(ev, imagesSize, no_stock_check);
|
||||
});
|
||||
|
||||
$container
|
||||
.find('input[name="add_qty"]')
|
||||
.change(function(ev){
|
||||
onChangeAddQuantity(ev, isWebsite, pricelistId);
|
||||
});
|
||||
|
||||
$container
|
||||
.find('button.js_add_cart_json')
|
||||
.click(onClickAddCartJSON);
|
||||
}
|
||||
|
||||
return {
|
||||
addConfiguratorEvents: addConfiguratorEvents,
|
||||
onChangeVariant: onChangeVariant,
|
||||
onChangeAddQuantity: onChangeAddQuantity,
|
||||
onClickAddCartJSON: onClickAddCartJSON,
|
||||
computePriceTotal: computePriceTotal
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
odoo.define('sale.ProductConfiguratorFormView', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ProductConfiguratorFormController = require('sale.ProductConfiguratorFormController');
|
||||
var ProductConfiguratorFormRenderer = require('sale.ProductConfiguratorFormRenderer');
|
||||
var FormView = require('web.FormView');
|
||||
var viewRegistry = require('web.view_registry');
|
||||
|
||||
var ProductConfiguratorFormView = FormView.extend({
|
||||
config: _.extend({}, FormView.prototype.config, {
|
||||
Controller: ProductConfiguratorFormController,
|
||||
Renderer: ProductConfiguratorFormRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('product_configurator_form', ProductConfiguratorFormView);
|
||||
|
||||
return ProductConfiguratorFormView;
|
||||
|
||||
});
|
||||
@@ -16,6 +16,10 @@
|
||||
&.active input {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
&.custom_value {
|
||||
background-image: linear-gradient(to bottom right, #FF0000, #FFF200, #1E9600);
|
||||
}
|
||||
}
|
||||
|
||||
.css_not_available_msg {
|
||||
@@ -63,6 +67,14 @@ label.css_attribute_color.css_not_available {
|
||||
.product_detail_img {
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
.variant_custom_value {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.variant_custom_value_label {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.configurator_container {
|
||||
@@ -86,6 +98,10 @@ label.css_attribute_color.css_not_available {
|
||||
}
|
||||
}
|
||||
|
||||
.modal.o_technical_modal .oe_optional_products_modal .btn.js_add_cart_json {
|
||||
padding: 0.075rem 0.75rem;
|
||||
}
|
||||
|
||||
.js_product {
|
||||
&.in_cart {
|
||||
.js_add_cart_variants {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
odoo.define('sale.product.configurator.tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var ProductConfiguratorFormView = require('sale.ProductConfiguratorFormView');
|
||||
var testUtils = require('web.test_utils');
|
||||
var ajax = require('web.ajax');
|
||||
var createView = testUtils.createView;
|
||||
|
||||
var getArch = function(){
|
||||
var getArch = function (){
|
||||
return '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="sale_order_line">' +
|
||||
@@ -44,18 +44,30 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
sale_order: {
|
||||
fields: {
|
||||
id: {type: 'integer'},
|
||||
sale_order_line: {string: 'lines', type: 'one2many', relation: 'sale_order_line'},
|
||||
sale_order_line: {
|
||||
string: 'lines',
|
||||
type: 'one2many',
|
||||
relation: 'sale_order_line'
|
||||
},
|
||||
}
|
||||
},
|
||||
sale_order_line: {
|
||||
fields: {
|
||||
product_id: {string: 'product', type: 'many2one', relation: 'product'},
|
||||
product_id: {
|
||||
string: 'product',
|
||||
type: 'many2one',
|
||||
relation: 'product'
|
||||
},
|
||||
quantity: {type: 'integer'}
|
||||
}
|
||||
},
|
||||
sale_product_configurator: {
|
||||
fields: {
|
||||
product_template_id: {string: 'product', type: 'many2one', relation: 'product_template'}
|
||||
product_template_id: {
|
||||
string: 'product',
|
||||
type: 'many2one',
|
||||
relation: 'product_template'
|
||||
}
|
||||
},
|
||||
records: [{
|
||||
product_template_id: 42
|
||||
@@ -63,10 +75,10 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function(){
|
||||
}, function (){
|
||||
QUnit.test('click on "Configure a product" and check for form loading', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'sale_order',
|
||||
@@ -80,7 +92,7 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
assert.strictEqual(form.$("a:contains('Configure a product')").length, 1);
|
||||
|
||||
form.$("a:contains('Configure a product')").click();
|
||||
@@ -88,7 +100,7 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
|
||||
QUnit.test('trigger_up the "add_record" event and checks that rows are correctly added to the list', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'sale_order',
|
||||
@@ -97,7 +109,7 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
});
|
||||
|
||||
var list = form.renderer.allFieldWidgets[form.handle][0];
|
||||
|
||||
|
||||
list.trigger_up('add_record', {
|
||||
context: [{product_id: 1, quantity: 2}, {product_id: 2, quantity: 3}],
|
||||
forceEditable: "bottom" ,
|
||||
@@ -107,19 +119,14 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
assert.strictEqual(list.$("tr.o_data_row").length, 2);
|
||||
});
|
||||
|
||||
QUnit.test('Select a product in the list and check for template loading', function(assert){
|
||||
QUnit.test('Select a product in the list and check for template loading', function (assert){
|
||||
assert.expect(1);
|
||||
|
||||
ajax.jsonRpc = function(route){
|
||||
assert.ok(true);
|
||||
return $.Deferred().then(_.constant($("<div></div>")));
|
||||
};
|
||||
|
||||
var product_configurator_form = createView({
|
||||
View: ProductConfiguratorFormView,
|
||||
model: 'sale_product_configurator',
|
||||
data: this.data,
|
||||
arch:
|
||||
arch:
|
||||
'<form js_class="product_configurator_form">' +
|
||||
'<group>' +
|
||||
'<field name="product_template_id" class="oe_product_configurator_product_template_id" />' +
|
||||
@@ -129,6 +136,13 @@ odoo.define('sale.product.configurator.tests', function (require) {
|
||||
'<button string="Cancel" class="btn-secondary" special="cancel"/>' +
|
||||
'</footer>' +
|
||||
'</form>',
|
||||
mockRPC: function (route) {
|
||||
if (route === '/product_configurator/configure') {
|
||||
assert.ok(true);
|
||||
return $.Deferred().then(_.constant(1));
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
var div = '<div>';
|
||||
var $div = $(div).addClass('js_sale_order_pricelist_id').html(43);
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/sale/static/src/js/sale.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/tour.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_list_editable_renderer.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_utils.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/list_editable_renderer.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_renderer.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_controller.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_view.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="product_product_attribute_value_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">product.product.attribute.value.view.form.inherit</field>
|
||||
<field name="model">product.product.attribute.value</field>
|
||||
@@ -70,6 +70,7 @@
|
||||
<field name="inherit_id" ref="product.variants_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="is_custom" groups="product.group_product_variant" />
|
||||
<field name="html_color"/>
|
||||
</field>
|
||||
</field>
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="optional_products_modal" name="Optional Products">
|
||||
<div role="dialog" id="modal_optional_products" class="modal fade" tabindex="-1" aria-labelledby="myModalLabel">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title" id="myModalLabel">Add to cart</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">x</button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<t t-call="sale.product_configurator_configure_optional_products" />
|
||||
</div>
|
||||
<footer class="modal-footer">
|
||||
<a href="#" role="button" class="btn btn-secondary a-submit go-back js_goto_shop" aria-label="Continue shopping" title="Continue shopping"><i class="fa fa-chevron-left"></i> <span class="d-none d-md-inline">Continue Shopping</span><span class="d-inline d-md-none">Continue</span></a>
|
||||
<a href="#" role="button" class="btn btn-primary float-right confirm a-submit" aria-label="Proceed to checkout" title="Proceed to checkout"><i class="fa fa-shopping-cart fa-fw"></i> <span class="d-none d-md-inline">Proceed to Checkout</span><span class="d-inline d-md-none">Checkout</span></a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main class="modal-body">
|
||||
<t t-call="sale.product_configurator_configure_optional_products" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<template id="product_configurator_configure" name="Configure">
|
||||
@@ -68,7 +54,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="js_product in_cart">
|
||||
<tr class="js_product in_cart main_product">
|
||||
<td class='td-img'>
|
||||
<span t-field="product.image_medium" t-options='{"widget": "image" }'/>
|
||||
</td>
|
||||
@@ -136,7 +122,7 @@
|
||||
<template id="optional_product_items">
|
||||
<t t-foreach="product.optional_product_ids" t-as="product">
|
||||
<t t-if="len(product.get_filtered_variants(reference_product)) > 0">
|
||||
<tr class="js_product">
|
||||
<tr class="js_product">
|
||||
<td class="td-img">
|
||||
<input type="hidden" class="product_template_id" t-att-value="product.id"/>
|
||||
<input type="hidden" class="product_id" t-attf-name="optional-product-#{product.id}" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'"/>
|
||||
@@ -189,14 +175,17 @@
|
||||
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product, reference_product, pricelist)"/>
|
||||
<ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_value_ids="json.dumps(attribute_value_ids)">
|
||||
<t t-foreach="product.attribute_line_ids.sorted(key=lambda x: x.attribute_id.sequence)" t-as="variant_id">
|
||||
<li t-if="len(variant_id.product_value_ids) > 1">
|
||||
<li t-if="len(variant_id.product_value_ids) > 1" t-att-data-attribute_id="variant_id.attribute_id.id" t-att-data-attribute_name="variant_id.attribute_id.name">
|
||||
|
||||
<strong t-field="variant_id.attribute_id.name"/>
|
||||
|
||||
<t t-if="variant_id.attribute_id.type == 'select'">
|
||||
<select t-attf-class="form-control #{'js_variant_change' if variant_id.attribute_id.create_variant else ''}" t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)">
|
||||
<t t-foreach="variant_id.product_value_ids" t-as="value_id">
|
||||
<option t-att-value="value_id.id">
|
||||
<option t-att-value="value_id.id"
|
||||
t-att-data-value_id="value_id.product_attribute_value_id.id"
|
||||
t-att-data-value_name="value_id.name"
|
||||
t-att-data-is_custom="value_id.is_custom">
|
||||
<span t-field="value_id.name"/>
|
||||
<span t-if="value_id.price_extra">
|
||||
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" class="variant_price_extra" style="white-space: nowrap;" t-options='{
|
||||
@@ -216,7 +205,15 @@
|
||||
<t t-foreach="variant_id.product_value_ids" t-as="value_id">
|
||||
<li class="form-group js_attribute_value" style="margin: 0;">
|
||||
<label class="col-form-label" style="margin: 0 20px;">
|
||||
<input type="radio" t-att-class="'js_variant_change' if variant_id.attribute_id.create_variant else None" t-att-checked="'checked' if not inc else None" t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" t-att-value="value_id.id" style="vertical-align: top; margin-right: 10px;"/>
|
||||
<input type="radio"
|
||||
t-att-class="'js_variant_change' if variant_id.attribute_id.create_variant else None"
|
||||
t-att-checked="'checked' if not inc else None"
|
||||
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"
|
||||
t-att-value="value_id.id"
|
||||
t-att-data-value_id="value_id.product_attribute_value_id.id"
|
||||
t-att-data-value_name="value_id.name"
|
||||
t-att-data-is_custom="value_id.is_custom"
|
||||
style="vertical-align: top; margin-right: 10px;"/>
|
||||
<span t-field="value_id.name"/>
|
||||
<span class="badge badge-pill" t-if="value_id.price_extra">
|
||||
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" class="variant_price_extra" style="white-space: nowrap;" t-options='{
|
||||
@@ -236,13 +233,16 @@
|
||||
<ul class="list-inline">
|
||||
<t t-set="inc" t-value="0"/>
|
||||
<li t-foreach="variant_id.product_value_ids" t-as="value_id" class="list-inline-item">
|
||||
<label t-attf-style="background-color:#{value_id.html_color or value_id.name}"
|
||||
t-attf-class="css_attribute_color #{'active' if not inc else ''}">
|
||||
<label t-attf-style="background-color:#{value_id.html_color or value_id.name if not value_id.is_custom else ''}"
|
||||
t-attf-class="css_attribute_color #{'active' if not inc else ''} #{'custom_value' if value_id.is_custom else ''}">
|
||||
<input type="radio" t-att-class="'js_variant_change' if variant_id.attribute_id.create_variant else None"
|
||||
t-att-checked="'checked' if not inc else None"
|
||||
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"
|
||||
t-att-value="value_id.id"
|
||||
t-att-title="value_id.name"/>
|
||||
t-att-title="value_id.name"
|
||||
t-att-data-value_id="value_id.product_attribute_value_id.id"
|
||||
t-att-data-value_name="value_id.name"
|
||||
t-att-data-is_custom="value_id.is_custom"/>
|
||||
</label>
|
||||
<t t-set="inc" t-value="inc+1"/>
|
||||
</li>
|
||||
|
||||
@@ -423,6 +423,7 @@
|
||||
'default_description_sale': name
|
||||
}"
|
||||
/>
|
||||
<field name="product_custom_variant_values" invisible="1" />
|
||||
<field name="name" widget="section_and_note_text"/>
|
||||
<field
|
||||
name="product_uom_qty"
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
odoo.define('sale.product_configurator_tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('sale_product_configurator_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"], .oe_menu_toggler[data-menu-xmlid="sale.sale_menu_root"]'
|
||||
}, {
|
||||
trigger: ".o_list_button_add",
|
||||
extra_trigger: ".o_sale_order"
|
||||
}, {
|
||||
trigger: "a:contains('Configure a product')"
|
||||
}, {
|
||||
trigger: '.o_product_configurator .o_input_dropdown input',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'li a:contains("Customizable Desk")',
|
||||
in_modal: false,
|
||||
extra_trigger: 'ul.ui-autocomplete',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.configurator_container span:contains("Aluminium")',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: ".o_sale_product_configurator_add",
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'span:contains("Aluminium")',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.js_product:not(.in_cart) .js_add',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.js_product:not(.in_cart) .js_add', // not a mistake, this adds the option's option
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.a-submit:not(.js_goto_shop)',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
},
|
||||
// check that 3 products were added to the SO
|
||||
{
|
||||
trigger: 'td.o_data_cell:contains("[FURN_0098] Customizable Desk (White, Aluminium)")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function(){}
|
||||
}, {
|
||||
trigger: 'td.o_data_cell:contains("[E-COM13] Conference Chair (Aluminium)")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function(){}
|
||||
}, {
|
||||
trigger: 'td.o_data_cell:contains("Chair floor protection")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function(){}
|
||||
}]);
|
||||
|
||||
});
|
||||
|
||||
odoo.define('sale.product_configurator_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('sale_product_configurator_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"], .oe_menu_toggler[data-menu-xmlid="sale.sale_menu_root"]'
|
||||
}, {
|
||||
trigger: ".o_list_button_add",
|
||||
extra_trigger: ".o_sale_order"
|
||||
}, {
|
||||
trigger: "a:contains('Configure a product')"
|
||||
}, {
|
||||
trigger: '.o_product_configurator .o_input_dropdown input',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'li a:contains("Customizable Desk")',
|
||||
in_modal: false,
|
||||
extra_trigger: 'ul.ui-autocomplete',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.configurator_container span:contains("Aluminium")',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: ".o_sale_product_configurator_add",
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'span:contains("Aluminium")',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.js_add',
|
||||
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair))',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.js_add',
|
||||
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Warranty))',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.js_add',
|
||||
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection))',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'button span:contains(Confirm)',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
},
|
||||
// check that 3 products were added to the SO
|
||||
{
|
||||
trigger: 'td.o_data_cell:contains("[FURN_0098] Customizable Desk (White, Aluminium)")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function (){}
|
||||
}, {
|
||||
trigger: 'td.o_data_cell:contains("[E-COM13] Conference Chair (Aluminium)")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function (){}
|
||||
}, {
|
||||
trigger: 'td.o_data_cell:contains("Chair floor protection")',
|
||||
extra_trigger: 'div[name="order_line"]',
|
||||
in_modal: false,
|
||||
run: function (){}
|
||||
}]);
|
||||
|
||||
});
|
||||
|
||||
@@ -46,4 +46,13 @@
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_product_settings" name="Products" sequence="80"
|
||||
parent="sale.menu_sale_config"/>
|
||||
<menuitem id="menu_product_attribute_action"
|
||||
action="product.attribute_action"
|
||||
parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/>
|
||||
<menuitem id="menu_catalog_variants_action"
|
||||
action="product.variants_action"
|
||||
parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/>
|
||||
</odoo>
|
||||
|
||||
@@ -399,6 +399,7 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
add_qty=add_qty,
|
||||
set_qty=set_qty,
|
||||
attributes=self._filter_attributes(**kw),
|
||||
product_custom_variant_values=json.loads(kw.get('product_custom_variant_values'))
|
||||
)
|
||||
return request.redirect("/shop/cart")
|
||||
|
||||
@@ -1083,6 +1084,8 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
|
||||
attributes = self._filter_attributes(**kw)
|
||||
|
||||
product_custom_variant_values = json.loads(kw.get('product_custom_variant_values'))
|
||||
|
||||
value = {}
|
||||
if add_qty or set_qty:
|
||||
value = order._cart_update(
|
||||
@@ -1090,7 +1093,8 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
add_qty=add_qty,
|
||||
set_qty=set_qty,
|
||||
attributes=attributes,
|
||||
optional_product_ids=optional_product_ids
|
||||
optional_product_ids=optional_product_ids,
|
||||
product_custom_variant_values=self._get_custom_variant_value(int(product_id), product_custom_variant_values)
|
||||
)
|
||||
|
||||
# options have all time the same quantity
|
||||
@@ -1099,11 +1103,20 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
product_id=option_id,
|
||||
set_qty=value.get('quantity'),
|
||||
attributes=attributes,
|
||||
linked_line_id=value.get('line_id')
|
||||
linked_line_id=value.get('line_id'),
|
||||
product_custom_variant_values=self._get_custom_variant_value(option_id, product_custom_variant_values)
|
||||
)
|
||||
|
||||
return str(order.cart_quantity)
|
||||
|
||||
def _get_custom_variant_value(self, product_id, product_custom_variant_values):
|
||||
if product_custom_variant_values:
|
||||
for product_custom_variant_value in product_custom_variant_values:
|
||||
if product_custom_variant_value['product_id'] == product_id:
|
||||
return product_custom_variant_value['product_custom_variant_values']
|
||||
|
||||
return None
|
||||
|
||||
@http.route(['/product_configurator/show_optional_products_website'], type='json', auth="public", methods=['POST'], website=True)
|
||||
def show_optional_products_website(self, product_id, **kw):
|
||||
return self._show_optional_products(product_id, request.website.get_current_pricelist(), True, **kw)
|
||||
|
||||
@@ -72,6 +72,8 @@ class SaleOrder(models.Model):
|
||||
domain = [('order_id', '=', self.id), ('product_id', '=', product_id)]
|
||||
if line_id:
|
||||
domain += [('id', '=', line_id)]
|
||||
else:
|
||||
domain += [('product_custom_variant_values', '=', False)]
|
||||
|
||||
lines = self.env['sale.order.line'].sudo().search(domain)
|
||||
|
||||
@@ -116,7 +118,7 @@ class SaleOrder(models.Model):
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def _get_line_description(self, order_id, product_id, attributes=None):
|
||||
def _get_line_description(self, order_id, product_id, attributes=None, custom_values=None):
|
||||
if not attributes:
|
||||
attributes = {}
|
||||
|
||||
@@ -140,6 +142,9 @@ class SaleOrder(models.Model):
|
||||
if product.description_sale:
|
||||
name += '\n%s' % (product.description_sale)
|
||||
|
||||
if custom_values:
|
||||
name += ''.join(['\n%s: %s' % (custom_value['attribute_value_name'], custom_value['custom_value']) for custom_value in custom_values])
|
||||
|
||||
return name
|
||||
|
||||
@api.multi
|
||||
@@ -170,7 +175,15 @@ class SaleOrder(models.Model):
|
||||
# Create line if no line with product_id can be located
|
||||
if not order_line:
|
||||
values = self._website_product_id_change(self.id, product_id, qty=1)
|
||||
values['name'] = self._get_line_description(self.id, product_id, attributes=attributes)
|
||||
|
||||
custom_values = kwargs.get('product_custom_variant_values')
|
||||
if custom_values:
|
||||
values['product_custom_variant_values'] = [(0, 0, {
|
||||
'attribute_value_id': custom_value['attribute_value_id'],
|
||||
'custom_value': custom_value['custom_value']
|
||||
}) for custom_value in custom_values]
|
||||
|
||||
values['name'] = self._get_line_description(self.id, product_id, attributes=attributes, custom_values=custom_values)
|
||||
order_line = SaleOrderLineSudo.create(values)
|
||||
|
||||
try:
|
||||
|
||||
@@ -11,6 +11,7 @@ access_product_supplierinfo,product.supplierinfo.public,product.model_product_su
|
||||
access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0
|
||||
access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0
|
||||
access_product_product_attribute,product.product.attribute value public,product.model_product_product_attribute_value,,1,0,0,0
|
||||
access_product_product_attribute_custom_value,product.attribute.custom value,sale.model_product_attribute_custom_value,,1,0,0,0
|
||||
access_product_attribute_filter_line,product.attribute filter line public,product.model_product_attribute_filter_line,,1,0,0,0
|
||||
access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0
|
||||
access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -124,7 +124,7 @@ odoo.define('website_sale.website_sale', function (require) {
|
||||
'use strict';
|
||||
|
||||
var utils = require('web.utils');
|
||||
var ProductConfiguratorUtils = require('sale.product_configurator_utils');
|
||||
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var sAnimations = require('website.content.snippets.animation');
|
||||
@@ -132,7 +132,7 @@ require("website.content.zoomodoo");
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorMixin, {
|
||||
selector: '.oe_website_sale',
|
||||
read_events: {
|
||||
'change form .js_product:first input[name="add_qty"]': '_onChangeAddQuantity',
|
||||
@@ -140,7 +140,7 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
'touchend .js_publish': '_onMouseupPublish',
|
||||
'change .oe_cart input.js_quantity[data-product-id]': '_onChangeCartQuantity',
|
||||
'click .oe_cart a.js_add_suggested_products': '_onClickSuggestedProduct',
|
||||
'click a.js_add_cart_json, button.js_add_cart_json': '_onClickAddCartJSON',
|
||||
'click a.js_add_cart_json': '_onClickAddCartJSON',
|
||||
'click .a-submit': '_onClickSubmit',
|
||||
'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute',
|
||||
'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel',
|
||||
@@ -162,6 +162,8 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
|
||||
this._changeCartQuantity = _.debounce(this._changeCartQuantity.bind(this), 500);
|
||||
this._changeCountry = _.debounce(this._changeCountry.bind(this), 500);
|
||||
|
||||
this.isWebsite = true;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -176,9 +178,7 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
$('input.js_product_change', product).first().trigger('change');
|
||||
});
|
||||
|
||||
_.each(this.$('.js_add_cart_variants'), function (add_variant) {
|
||||
$('input.js_variant_change, select.js_variant_change', add_variant).first().trigger('change');
|
||||
});
|
||||
this.triggerVariantChange(this.$el);
|
||||
|
||||
this.$('select[name="country_id"]').change();
|
||||
|
||||
@@ -334,12 +334,19 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickAddCartJSON: function (ev){
|
||||
this.onClickAddCartJSON(ev);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onChangeAddQuantity: function (ev, isWebsite, pricelist, no_stock_check) {
|
||||
ProductConfiguratorUtils.onChangeAddQuantity(ev, true, pricelist, no_stock_check);
|
||||
_onChangeAddQuantity: function (ev, no_stock_check) {
|
||||
this.onChangeAddQuantity(ev, no_stock_check);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -375,13 +382,6 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
_onClickSuggestedProduct: function (ev) {
|
||||
$(ev.currentTarget).prev('input').val(1).trigger('change');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickAddCartJSON: function (ev) {
|
||||
ProductConfiguratorUtils.onClickAddCartJSON(ev);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
@@ -447,7 +447,7 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onChangeVariant: function (ev) {
|
||||
ProductConfiguratorUtils.onChangeVariant(ev, false);
|
||||
this.onChangeVariant(ev, false);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -1,88 +1,146 @@
|
||||
odoo.define('website_sale_options.website_sale', function (require) {
|
||||
'use strict';
|
||||
|
||||
var OptionalProductsModal = require('sale.OptionalProductsModal');
|
||||
var weContext = require('web_editor.context');
|
||||
var sAnimations = require('website.content.snippets.animation');
|
||||
require('website_sale.website_sale');
|
||||
|
||||
var product_name_map = {};
|
||||
var optional_products_map = {};
|
||||
|
||||
sAnimations.registry.WebsiteSaleOptions = sAnimations.Class.extend({
|
||||
selector: '.oe_website_sale',
|
||||
read_events: {
|
||||
'click #add_to_cart, #products_grid .product_price .a-submit': '_onClickAdd',
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._handleAdd = _.debounce(this._handleAdd.bind(this), 200, true);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleAdd: function ($form) {
|
||||
var self = this;
|
||||
var quantity = parseFloat($form.find('input[name="add_qty"]').val() || 1);
|
||||
var product_id = parseInt($form.find('input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked').first().val(),10);
|
||||
'use strict';
|
||||
|
||||
var modal = new OptionalProductsModal(quantity, product_id, $form, null, true);
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var OptionalProductsModal = require('sale.OptionalProductsModal');
|
||||
var weContext = require('web_editor.context');
|
||||
var sAnimations = require('website.content.snippets.animation');
|
||||
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
|
||||
require('website_sale.website_sale');
|
||||
|
||||
modal.on('options_empty', null, function () {
|
||||
$form.trigger('submit', [true]);
|
||||
});
|
||||
sAnimations.registry.WebsiteSaleOptions = sAnimations.Class.extend(ProductConfiguratorMixin, {
|
||||
selector: '.oe_website_sale',
|
||||
read_events: {
|
||||
'click #add_to_cart, #products_grid .product_price .a-submit': '_onClickAdd',
|
||||
},
|
||||
|
||||
modal.on('back', null, function () {
|
||||
self._onModalSubmit($form);
|
||||
});
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
modal.on('confirm', null, function () {
|
||||
self._onModalSubmit($form, true);
|
||||
});
|
||||
this._handleAdd = _.debounce(this._handleAdd.bind(this), 200, true);
|
||||
},
|
||||
|
||||
modal.on('modal_ready', null, function ($modal) {
|
||||
$modal.on('hidden.bs.modal', function () {
|
||||
$form.removeClass('css_options'); // possibly reactivate opacity (see above)
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
modal.appendTo($form);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onModalSubmit: function($form, go_to_shop){
|
||||
$form.ajaxSubmit({
|
||||
url: '/shop/cart/update_option',
|
||||
data: {lang: weContext.get().lang},
|
||||
success: function (quantity) {
|
||||
if (go_to_shop) {
|
||||
window.location.pathname = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart");
|
||||
}
|
||||
var $q = $(".my_cart_quantity");
|
||||
$q.parent().parent().removeClass("d-none", !quantity);
|
||||
$q.html(quantity).hide().fadeIn(600);
|
||||
/**
|
||||
* Prevents the default submit to allow showing product options
|
||||
* in a modal (if any)
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickAdd: function (ev) {
|
||||
ev.preventDefault();
|
||||
this._handleAdd($(ev.currentTarget).closest('form'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the optional products modal
|
||||
* and add handlers to the modal events (confirm, back, ...)
|
||||
*
|
||||
* @private
|
||||
* @param {$.Element} $form the related webshop form
|
||||
*/
|
||||
_handleAdd: function ($form) {
|
||||
this.$form = $form;
|
||||
|
||||
var productSelector = [
|
||||
'input[type="hidden"][name="product_id"]',
|
||||
'input[type="radio"][name="product_id"]:checked'
|
||||
];
|
||||
|
||||
this.rootProduct = {
|
||||
product_id: parseInt($form.find(productSelector.join(', ')).first().val(), 10),
|
||||
quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1),
|
||||
product_custom_variant_values: this.getCustomVariantValues($form.find('.js_product'))
|
||||
};
|
||||
|
||||
this.isWebsite = true;
|
||||
this.optionalProductsModal = new OptionalProductsModal($form, {
|
||||
rootProduct: this.rootProduct,
|
||||
isWebsite: true,
|
||||
okButtonText: _t('Proceed to Checkout'),
|
||||
cancelButtonText: _t('Continue Shopping'),
|
||||
title: _t('Add to cart')
|
||||
}).open();
|
||||
|
||||
this.optionalProductsModal.on('options_empty', null, this._onModalOptionsEmpty.bind(this));
|
||||
this.optionalProductsModal.on('confirm', null, this._onModalConfirm.bind(this));
|
||||
this.optionalProductsModal.on('back', null, this._onModalBack.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* No optional products found for this product
|
||||
* Add custom variant values in the form data and trigger submit
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onModalOptionsEmpty: function () {
|
||||
var $productCustomVariantValues = $('<input>', {
|
||||
name: 'product_custom_variant_values',
|
||||
type: "hidden",
|
||||
value: JSON.stringify(this.rootProduct.product_custom_variant_values)
|
||||
});
|
||||
this.$form.append($productCustomVariantValues);
|
||||
this.$form.trigger('submit', [true]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit form and stay on the same page
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onModalBack: function () {
|
||||
this._onModalSubmit(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit form and go to shop
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onModalConfirm: function () {
|
||||
this._onModalSubmit(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submits the form with additional parameters
|
||||
* - lang
|
||||
* - product_custom_variant_values: The products custom variant values
|
||||
*
|
||||
* @private
|
||||
* @param {Boolean} goToShop Triggers a page refresh to the url "shop/cart"
|
||||
*/
|
||||
_onModalSubmit: function (goToShop){
|
||||
var productCustomVariantValues = JSON.stringify(
|
||||
this.optionalProductsModal.getSelectedProducts()
|
||||
);
|
||||
|
||||
this.$form.ajaxSubmit({
|
||||
url: '/shop/cart/update_option',
|
||||
data: {
|
||||
lang: weContext.get().lang,
|
||||
product_custom_variant_values: productCustomVariantValues
|
||||
},
|
||||
success: function (quantity) {
|
||||
if (goToShop) {
|
||||
var path = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart");
|
||||
window.location.pathname = path;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onClickAdd: function (ev) {
|
||||
ev.preventDefault();
|
||||
this._handleAdd($(ev.currentTarget).closest('form'));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var $quantity = $(".my_cart_quantity");
|
||||
$quantity.parent().parent().removeClass("d-none", !quantity);
|
||||
$quantity.html(quantity).hide().fadeIn(600);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return sAnimations.registry.WebsiteSaleOptions;
|
||||
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ tour.register('shop_buy_product', {
|
||||
},
|
||||
{
|
||||
content: "click in modal on 'Proceed to checkout' button",
|
||||
trigger: 'a:contains("Proceed to Checkout")',
|
||||
trigger: 'button:contains("Proceed to Checkout")',
|
||||
},
|
||||
{
|
||||
content: "add suggested",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
odoo.define("website_sale.tour_shop", function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require("web_tour.tour");
|
||||
var base = require("web_editor.base");
|
||||
|
||||
tour.register("shop_custom_attribute_value", {
|
||||
url: "/shop",
|
||||
test: true,
|
||||
wait_for: base.ready()
|
||||
}, [{
|
||||
trigger: 'img[src*="/product.template/9"]',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'li.js_attribute_value span:contains(Custom)',
|
||||
extra_trigger: 'li.js_attribute_value',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input.variant_custom_value',
|
||||
run: 'text Wood',
|
||||
}, {
|
||||
trigger: 'a:contains(Add to Cart)',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div:contains(Custom: Wood)',
|
||||
extra_trigger: '.js_product.in_cart.main_product',
|
||||
run: function (){} // checks that Yep, it's wood!
|
||||
}, {
|
||||
trigger: 'button.js_add_cart_json:has(i.fa-plus)',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.oe_striked_price span:contains(750)',
|
||||
run: function (){}, // check
|
||||
}, {
|
||||
trigger: 'span.oe_price span:contains(600)',
|
||||
run: function (){}, // check
|
||||
}, {
|
||||
trigger: 'li.js_attribute_value span:contains(Aluminium)',
|
||||
extra_trigger: '.oe_optional_products_modal',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.oe_price span:contains(22.9)',
|
||||
run: function (){}, // check
|
||||
}, {
|
||||
trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair)) .js_add',
|
||||
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair))',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection)) .js_add',
|
||||
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection))',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: 'span:contains(1,269.80)',
|
||||
run: function (){}, // check
|
||||
}, {
|
||||
trigger: 'button:has(span:contains(Proceed to Checkout))',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'span:contains(Custom: Wood)',
|
||||
extra_trigger: '#cart_products',
|
||||
run: function (){}, // check
|
||||
}]);
|
||||
});
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale_options.scss"/>
|
||||
</xpath>
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_utils.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_utils.js"></script>
|
||||
@@ -28,6 +28,7 @@
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_buy.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_customize.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_options.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user