diff --git a/addons/website_sale/controllers/__init__.py b/addons/website_sale/controllers/__init__.py index 5ee7aaab8f2..9632bd1bc39 100644 --- a/addons/website_sale/controllers/__init__.py +++ b/addons/website_sale/controllers/__init__.py @@ -3,3 +3,4 @@ from . import backend from . import main +from . import variant diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index d22b69b8636..c9dc402c2c9 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -12,7 +12,6 @@ from odoo.addons.payment.controllers.portal import PaymentProcessing from odoo.addons.website.controllers.main import QueryURL from odoo.exceptions import ValidationError from odoo.addons.website.controllers.main import Website -from odoo.addons.sale.controllers.product_configurator import ProductConfiguratorController from odoo.addons.website_form.controllers.main import WebsiteForm from odoo.osv import expression @@ -131,7 +130,7 @@ class Website(Website): return views -class WebsiteSale(ProductConfiguratorController): +class WebsiteSale(http.Controller): def _get_compute_currency_and_context(self, product=None): pricelist_context, pricelist = self._get_pricelist_context() @@ -292,6 +291,9 @@ class WebsiteSale(ProductConfiguratorController): if not product.can_access_from_current_website(): raise NotFound() + return request.render("website_sale.product", self._prepare_product_values(product, category, search, **kwargs)) + + def _prepare_product_values(self, product, category, search, **kwargs): add_qty = int(kwargs.get('add_qty', 1)) product_context = dict(request.env.context, quantity=add_qty, @@ -319,7 +321,7 @@ class WebsiteSale(ProductConfiguratorController): product_context['pricelist'] = pricelist.id product = product.with_context(product_context) - values = { + return { 'search': search, 'category': category, 'pricelist': pricelist, @@ -332,11 +334,7 @@ class WebsiteSale(ProductConfiguratorController): 'main_object': product, 'product': product, 'add_qty': add_qty, - 'optional_product_ids': [p.with_context({'active_id': p.id}) for p in product.optional_product_ids], - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions, } - return request.render("website_sale.product", values) @http.route(['/shop/change_pricelist/'], type='http', auth="public", website=True, sitemap=False) def pricelist_change(self, pl_id, **post): @@ -1121,109 +1119,3 @@ class WebsiteSale(ProductConfiguratorController): lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US') return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\ .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}') - - @http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False) - def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw): - """This route is called when submitting the optional product modal.""" - if lang: - request.website = request.website.with_context(lang=lang) - - order = request.website.sale_get_order(force_create=True) - if order.state != 'draft': - request.session['sale_order_id'] = None - order = request.website.sale_get_order(force_create=True) - optional_product_ids = [] - for k, v in kw.items(): - if "optional-product-" in k and int(kw.get(k.replace("product", "add"))): - optional_product_ids.append(int(v)) - - custom_values = [] - if kw.get('custom_values'): - custom_values = json.loads(kw.get('custom_values')) - - value = {} - if add_qty or set_qty: - value = order._cart_update( - product_id=int(product_id), - add_qty=add_qty, - set_qty=set_qty, - optional_product_ids=optional_product_ids, - product_custom_attribute_values=self._get_product_custom_value( - int(product_id), - custom_values, - 'product_custom_attribute_values' - ), - no_variant_attribute_values=self._get_product_custom_value( - int(product_id), - custom_values, - 'no_variant_attribute_values' - ) - ) - - # options have all time the same quantity - for option_id in optional_product_ids: - order._cart_update( - product_id=option_id, - set_qty=value.get('quantity'), - linked_line_id=value.get('line_id'), - product_custom_attribute_values=self._get_product_custom_value( - option_id, - custom_values, - 'product_custom_attribute_values' - ), - no_variant_attribute_values=self._get_product_custom_value( - option_id, - custom_values, - 'no_variant_attribute_values' - ) - ) - - return str(order.cart_quantity) - - def _get_product_custom_value(self, product_id, custom_values, field): - if custom_values: - for custom_value in custom_values: - if custom_value['product_id'] == product_id: - return custom_value[field] - - return None - - # ------------------------------------------------------ - # Product Configurator - # ------------------------------------------------------ - - @http.route(auth="public") - def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): - """Override because on the website the public user must access it.""" - return super(WebsiteSale, self).create_product_variant(product_template_id, product_template_attribute_value_ids, **kwargs) - - @http.route(['/product_configurator/show_optional_products_website'], type='json', auth="public", methods=['POST'], website=True) - def show_optional_products_website(self, product_id, variant_values, **kw): - """Special route to use website logic in get_combination_info override. - This route is called in JS by appending _website to the base route. - """ - kw.pop('pricelist_id') - return self.show_optional_products(product_id, variant_values, request.website.get_current_pricelist(), **kw) - - @http.route(['/product_configurator/optional_product_items_website'], type='json', auth="public", methods=['POST'], website=True) - def optional_product_items_website(self, product_id, **kw): - """Special route to use website logic in get_combination_info override. - This route is called in JS by appending _website to the base route. - """ - kw.pop('pricelist_id') - return self.optional_product_items(product_id, request.website.get_current_pricelist(), **kw) - - @http.route(['/product_configurator/get_combination_info_website'], type='json', auth="public", methods=['POST'], website=True) - def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw): - """Special route to use website logic in get_combination_info override. - This route is called in JS by appending _website to the base route. - """ - kw.pop('pricelist_id') - res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw) - carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel', - values={ - 'product': request.env['product.template'].browse(res['product_template_id']), - 'product_variant': request.env['product.product'].browse(res['product_id']), - }) - res['carousel'] = carousel_view - return res diff --git a/addons/website_sale/controllers/variant.py b/addons/website_sale/controllers/variant.py new file mode 100644 index 00000000000..76069d26bc5 --- /dev/null +++ b/addons/website_sale/controllers/variant.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import http +from odoo.http import request + +from odoo.addons.sale.controllers.variant import VariantController + +class WebsiteSaleVariantController(VariantController): + @http.route(['/sale/get_combination_info_website'], type='json', auth="public", methods=['POST'], website=True) + def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw): + """Special route to use website logic in get_combination_info override. + This route is called in JS by appending _website to the base route. + """ + kw.pop('pricelist_id') + res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw) + + carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel', + values={ + 'product': request.env['product.template'].browse(res['product_template_id']), + 'product_variant': request.env['product.product'].browse(res['product_id']), + }) + res['carousel'] = carousel_view + return res + + @http.route(auth="public") + def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): + """Override because on the website the public user must access it.""" + return super(WebsiteSaleVariantController, self).create_product_variant(product_template_id, product_template_attribute_value_ids, **kwargs) diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml index 49b94a1701b..3e26dbed843 100644 --- a/addons/website_sale/data/demo.xml +++ b/addons/website_sale/data/demo.xml @@ -5,11 +5,6 @@ - - 100 - - - @@ -663,9 +658,6 @@ - - - diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 54eb4bce462..b264cbf2585 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -240,6 +240,7 @@ class ProductTemplate(models.Model): if not self._is_add_to_cart_possible(parent_combination): return False + # TODO FIXME awa: not optimal since rebased on p. configurator v2 gen = self._get_possible_combinations(parent_combination) first_possible_combination = next(gen) if next(gen, False) is not False: @@ -247,8 +248,6 @@ class ProductTemplate(models.Model): return False if self._has_is_custom_values(): return False - if self.optional_product_ids.filtered(lambda p: p._is_add_to_cart_possible(first_possible_combination)): - return False return True @api.multi diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 0d95368a223..3721932bfb5 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -95,17 +95,8 @@ class SaleOrder(models.Model): if line_id: return lines - linked_line_id = kwargs.get('linked_line_id', False) - optional_product_ids = set(kwargs.get('optional_product_ids', [])) - lines = lines.filtered(lambda line: line.linked_line_id.id == linked_line_id) - if optional_product_ids: - # only match the lines with the same chosen optional products on the existing lines - lines = lines.filtered(lambda line: optional_product_ids == set(line.mapped('option_line_ids.product_id.id'))) - else: - lines = lines.filtered(lambda line: not line.option_line_ids) - - return lines + return self.env['sale.order.line'] @api.multi def _website_product_id_change(self, order_id, product_id, qty=0): diff --git a/addons/website_sale/static/src/js/product_configurator_mixin.js b/addons/website_sale/static/src/js/product_configurator_mixin.js new file mode 100644 index 00000000000..683ee27c465 --- /dev/null +++ b/addons/website_sale/static/src/js/product_configurator_mixin.js @@ -0,0 +1,23 @@ +odoo.define('website_sale_.ProductConfiguratorMixin', function (require) { +'use strict'; + +var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin'); + +/** + * 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 + */ +ProductConfiguratorMixin._getUri = function (uri) { + if (this.isWebsite){ + return uri + '_website'; + } else { + return uri; + } +}; + +return ProductConfiguratorMixin; + +}); diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index c4ff6d79e14..38e78bad9f2 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -143,6 +143,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(ProductConfigurat 'change select[name="country_id"]': '_onChangeCountry', 'change #shipping_use_same': '_onChangeShippingUseSame', 'click .toggle_summary': '_onToggleSummary', + 'click #add_to_cart, #products_grid .product_price .a-submit': 'async _onClickAdd', 'click input.js_product_change': 'onChangeVariant', // dirty fix: prevent options modal events to be triggered and bubbled 'change oe_optional_products_modal [data-attribute_exclusions]': 'onChangeVariant', @@ -421,6 +422,83 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(ProductConfigurat // Handlers //-------------------------------------------------------------------------- + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAdd: function (ev) { + ev.preventDefault(); + return 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) { + var self = this; + this.$form = $form; + + var productSelector = [ + 'input[type="hidden"][name="product_id"]', + 'input[type="radio"][name="product_id"]:checked' + ]; + + var productReady = this.selectOrCreateProduct( + $form, + parseInt($form.find(productSelector.join(', ')).first().val(), 10), + $form.find('.product_template_id').val(), + false + ); + + productReady.then(function (productId){ + $form.find(productSelector.join(', ')).val(productId); + + self.rootProduct = { + product_id: productId, + quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1), + product_custom_attribute_values: self.getCustomVariantValues($form.find('.js_product')), + variant_values: self.getSelectedVariantValues($form.find('.js_product')), + no_variant_attribute_values: self.getNoVariantAttributeValues($form.find('.js_product')) + }; + + return self._onProductReady(); + }); + + return productReady; + }, + + _onProductReady: function () { + this._submitForm(); + }, + + /** + * Add custom variant values and attribute values that do not generate variants + * in the form data and trigger submit. + * + * @private + */ + _submitForm: function () { + var $productCustomVariantValues = $('', { + name: 'product_custom_attribute_values', + type: "hidden", + value: JSON.stringify(this.rootProduct.product_custom_attribute_values) + }); + this.$form.append($productCustomVariantValues); + + var $productNoVariantAttributeValues = $('', { + name: 'no_variant_attribute_values', + type: "hidden", + value: JSON.stringify(this.rootProduct.no_variant_attribute_values) + }); + this.$form.append($productNoVariantAttributeValues); + + this.$form.trigger('submit', [true]); + }, + /** * @private * @param {MouseEvent} ev diff --git a/addons/website_sale/static/src/js/website_sale_options.js b/addons/website_sale/static/src/js/website_sale_options.js deleted file mode 100644 index 5e55afe0e18..00000000000 --- a/addons/website_sale/static/src/js/website_sale_options.js +++ /dev/null @@ -1,193 +0,0 @@ -odoo.define('website_sale_options.website_sale', function (require) { -'use strict'; - -var core = require('web.core'); -var publicWidget = require('web.public.widget'); -var OptionalProductsModal = require('sale.OptionalProductsModal'); -var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin'); -require('website_sale.website_sale'); - -var _t = core._t; - -publicWidget.registry.WebsiteSaleOptions = publicWidget.Widget.extend(ProductConfiguratorMixin, { - selector: '.oe_website_sale', - events: _.extend({}, ProductConfiguratorMixin.events || {}, { - 'click #add_to_cart, #products_grid .product_price .a-submit': 'async _onClickAdd', - }), - - /** - * @constructor - */ - init: function () { - this._super.apply(this, arguments); - - this.isWebsite = true; - - delete this.events['change [data-attribute_exclusions]']; - delete this.events['click input.js_product_change']; - delete this.events['change input.js_quantity']; - delete this.events['click button.js_add_cart_json']; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Prevents the default submit to allow showing product options - * in a modal (if any) - * - * @private - * @param {MouseEvent} ev - */ - _onClickAdd: function (ev) { - ev.preventDefault(); - return 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) { - var self = this; - this.$form = $form; - this.isWebsite = true; - - var productSelector = [ - 'input[type="hidden"][name="product_id"]', - 'input[type="radio"][name="product_id"]:checked' - ]; - - var productReady = this.selectOrCreateProduct( - $form, - parseInt($form.find(productSelector.join(', ')).first().val(), 10), - $form.find('.product_template_id').val(), - false - ); - - productReady.then(function (productId){ - $form.find(productSelector.join(', ')).val(productId); - - self.rootProduct = { - product_id: productId, - quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1), - product_custom_attribute_values: self.getCustomVariantValues($form.find('.js_product')), - variant_values: self.getSelectedVariantValues($form.find('.js_product')), - no_variant_attribute_values: self.getNoVariantAttributeValues($form.find('.js_product')) - }; - - self.optionalProductsModal = new OptionalProductsModal($form, { - rootProduct: self.rootProduct, - isWebsite: true, - okButtonText: _t('Proceed to Checkout'), - cancelButtonText: _t('Continue Shopping'), - title: _t('Add to cart'), - context: self._getContext(), - }).open(); - - self.optionalProductsModal.on('options_empty', null, self._onModalOptionsEmpty.bind(self)); - self.optionalProductsModal.on('update_quantity', null, self._onOptionsUpdateQuantity.bind(self)); - self.optionalProductsModal.on('confirm', null, self._onModalConfirm.bind(self)); - self.optionalProductsModal.on('back', null, self._onModalBack.bind(self)); - - return self.optionalProductsModal.opened(); - }); - - return productReady; - }, - - /** - * No optional products found for this product - * Add custom variant values and attribute values that do not generate variants - * in the form data and trigger submit - * - * @private - */ - _onModalOptionsEmpty: function () { - var $productCustomVariantValues = $('', { - name: 'product_custom_attribute_values', - type: "hidden", - value: JSON.stringify(this.rootProduct.product_custom_attribute_values) - }); - this.$form.append($productCustomVariantValues); - - var $productNoVariantAttributeValues = $('', { - name: 'no_variant_attribute_values', - type: "hidden", - value: JSON.stringify(this.rootProduct.no_variant_attribute_values) - }); - this.$form.append($productNoVariantAttributeValues); - - this.$form.trigger('submit', [true]); - }, - - /** - * Update web shop base form quantity - * when quantity is updated in the optional products window - * - * @private - * @param {integer} quantity - */ - _onOptionsUpdateQuantity: function (quantity) { - this.$form - .find('input[name="add_qty"]') - .first() - .val(quantity) - .trigger('change'); - }, - - /** - * 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_attribute_values: The products custom variant values - * - * @private - * @param {Boolean} goToShop Triggers a page refresh to the url "shop/cart" - */ - _onModalSubmit: function (goToShop){ - var customValues = JSON.stringify( - this.optionalProductsModal.getSelectedProducts() - ); - - this.$form.ajaxSubmit({ - url: '/shop/cart/update_option', - data: { - lang: this._getContext().lang, - custom_values: customValues - }, - success: function (quantity) { - if (goToShop) { - var path = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart"); - window.location.pathname = path; - } - var $quantity = $(".my_cart_quantity"); - $quantity.parent().parent().removeClass("d-none", !quantity); - $quantity.html(quantity).hide().fadeIn(600); - } - }); - }, -}); -return publicWidget.registry.WebsiteSaleOptions; -}); diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 9890566f03b..7ead06ac14b 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -30,7 +30,7 @@
-