From 3cb46ec74ca8e8d96b358ccce3afc32ff4986bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Tue, 19 Mar 2019 13:40:48 +0000 Subject: [ADD/MOV] sale_product_configurator: new module and files move Purpose ======= - Moved the product_configurator into its own module now that we have a specific option for it. The option only enables the p. configurator in the SO form. The e-commerce should always allow customer to configure their products, even if the option is not checked. This allows moving some of the code specific to the p. configurator into its own module. However, a lot of the code has to remain in "sale" module since the e-commerce does not require the option checked and needs the "configure product" feature in the shop by default. (This is why the fields "product_custom_attribute_value_ids" and "product_no_variant_attribute_value_ids" are still in the "sale" module) --- addons/product/models/res_config_settings.py | 14 + addons/sale/__manifest__.py | 3 +- addons/sale/controllers/__init__.py | 2 +- addons/sale/controllers/product_configurator.py | 149 ------- addons/sale/controllers/variant.py | 28 ++ addons/sale/data/sale_demo.xml | 17 - addons/sale/models/product_product.py | 10 - .../src/js/product_configurator_controller.js | 199 --------- .../static/src/js/product_configurator_mixin.js | 4 +- .../static/src/js/product_configurator_modal.js | 483 --------------------- .../static/src/js/product_configurator_renderer.js | 54 --- .../static/src/js/product_configurator_view.js | 20 - .../sale/static/tests/product_configurator.test.js | 172 -------- addons/sale/views/assets.xml | 6 - addons/sale/views/res_config_settings_views.xml | 13 +- .../views/sale_product_configurator_templates.xml | 312 ------------- addons/sale/views/sale_views.xml | 7 - addons/sale/views/variant_templates.xml | 108 +++++ addons/sale/wizard/__init__.py | 1 - addons/sale/wizard/sale_product_configurator.py | 14 - .../wizard/sale_product_configurator_views.xml | 18 - addons/sale_management/__manifest__.py | 1 - .../tests/product_configurator_advanced_test_ui.js | 160 ------- .../product_configurator_pricelist_test_ui.js | 100 ----- .../static/tests/product_configurator_test_ui.js | 99 ----- addons/sale_management/tests/test_sale_ui.py | 116 ----- addons/sale_management/views/assets.xml | 10 - addons/sale_product_configurator/__init__.py | 5 + addons/sale_product_configurator/__manifest__.py | 26 ++ .../controllers/__init__.py | 4 + .../sale_product_configurator/controllers/main.py | 114 +++++ .../sale_product_configurator/data/sale_demo.xml | 25 ++ addons/sale_product_configurator/models/product.py | 14 + .../src/js/product_configurator_controller.js | 198 +++++++++ .../static/src/js/product_configurator_modal.js | 483 +++++++++++++++++++++ .../static/src/js/product_configurator_renderer.js | 54 +++ .../static/src/js/product_configurator_view.js | 20 + .../static/tests/product_configurator.test.js | 172 ++++++++ .../tests/product_configurator_advanced_test_ui.js | 160 +++++++ .../product_configurator_pricelist_test_ui.js | 100 +++++ .../static/tests/product_configurator_test_ui.js | 99 +++++ addons/sale_product_configurator/tests/__init__.py | 4 + .../tests/test_sale_product_configurator_ui.py | 159 +++++++ addons/sale_product_configurator/views/assets.xml | 27 ++ .../sale_product_configurator/views/sale_views.xml | 17 + .../sale_product_configurator/views/templates.xml | 204 +++++++++ .../wizard/sale_product_configurator.py | 14 + .../wizard/sale_product_configurator_views.xml | 18 + .../views/res_config_settings_views.xml | 11 + 49 files changed, 2094 insertions(+), 1954 deletions(-) delete mode 100644 addons/sale/controllers/product_configurator.py create mode 100644 addons/sale/controllers/variant.py delete mode 100644 addons/sale/static/src/js/product_configurator_controller.js delete mode 100644 addons/sale/static/src/js/product_configurator_modal.js delete mode 100644 addons/sale/static/src/js/product_configurator_renderer.js delete mode 100644 addons/sale/static/src/js/product_configurator_view.js delete mode 100644 addons/sale/static/tests/product_configurator.test.js delete mode 100644 addons/sale/views/sale_product_configurator_templates.xml create mode 100644 addons/sale/views/variant_templates.xml delete mode 100644 addons/sale/wizard/sale_product_configurator.py delete mode 100644 addons/sale/wizard/sale_product_configurator_views.xml delete mode 100644 addons/sale_management/static/tests/product_configurator_advanced_test_ui.js delete mode 100644 addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js delete mode 100644 addons/sale_management/static/tests/product_configurator_test_ui.js delete mode 100644 addons/sale_management/views/assets.xml create mode 100644 addons/sale_product_configurator/__init__.py create mode 100644 addons/sale_product_configurator/__manifest__.py create mode 100644 addons/sale_product_configurator/controllers/__init__.py create mode 100644 addons/sale_product_configurator/controllers/main.py create mode 100644 addons/sale_product_configurator/data/sale_demo.xml create mode 100644 addons/sale_product_configurator/models/product.py create mode 100644 addons/sale_product_configurator/static/src/js/product_configurator_controller.js create mode 100644 addons/sale_product_configurator/static/src/js/product_configurator_modal.js create mode 100644 addons/sale_product_configurator/static/src/js/product_configurator_renderer.js create mode 100644 addons/sale_product_configurator/static/src/js/product_configurator_view.js create mode 100644 addons/sale_product_configurator/static/tests/product_configurator.test.js create mode 100644 addons/sale_product_configurator/static/tests/product_configurator_advanced_test_ui.js create mode 100644 addons/sale_product_configurator/static/tests/product_configurator_pricelist_test_ui.js create mode 100644 addons/sale_product_configurator/static/tests/product_configurator_test_ui.js create mode 100644 addons/sale_product_configurator/tests/__init__.py create mode 100644 addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py create mode 100644 addons/sale_product_configurator/views/assets.xml create mode 100644 addons/sale_product_configurator/views/sale_views.xml create mode 100644 addons/sale_product_configurator/views/templates.xml create mode 100644 addons/sale_product_configurator/wizard/sale_product_configurator.py create mode 100644 addons/sale_product_configurator/wizard/sale_product_configurator_views.xml diff --git a/addons/product/models/res_config_settings.py b/addons/product/models/res_config_settings.py index 7e73f4df5e6..6272b1f37ba 100644 --- a/addons/product/models/res_config_settings.py +++ b/addons/product/models/res_config_settings.py @@ -14,6 +14,7 @@ class ResConfigSettings(models.TransientModel): " * Unchecked : Each company can see only its product (product where company is defined). Product not related to a company are visible for all companies.") group_uom = fields.Boolean("Units of Measure", implied_group='uom.group_uom') group_product_variant = fields.Boolean("Variants and Options", implied_group='product.group_product_variant') + module_sale_product_configurator = fields.Boolean("Product Configurator") group_stock_packaging = fields.Boolean('Product Packagings', implied_group='product.group_stock_packaging') group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers", @@ -33,6 +34,19 @@ class ResConfigSettings(models.TransientModel): ('1', 'Cubic Feet'), ], 'Volume unit of measure', config_parameter='product.volume_in_cubic_feet', default='0') + @api.onchange('group_product_variant') + def _onchange_group_product_variant(self): + """The product Configurator requires the product variants activated. + If the user disables the product variants -> disable the product configurator as well""" + if self.module_sale_product_configurator and not self.group_product_variant: + self.module_sale_product_configurator = False + + @api.onchange('module_sale_product_configurator') + def _onchange_module_sale_product_configurator(self): + """The product Configurator requires the product variants activated + If the user enables the product configurator -> enable the product variants as well""" + if self.module_sale_product_configurator and not self.group_product_variant: + self.group_product_variant = True @api.model def get_values(self): diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index 741d357c900..c275df0ac9b 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -27,13 +27,12 @@ This module contains all the common features of Sales Management and eCommerce. 'views/res_partner_views.xml', 'views/mail_activity_views.xml', 'views/assets.xml', + 'views/variant_templates.xml', 'views/sale_portal_templates.xml', - 'views/sale_product_configurator_templates.xml', 'views/sale_onboarding_views.xml', 'views/res_config_settings_views.xml', 'views/payment_views.xml', 'views/product_attribute_views.xml', - 'wizard/sale_product_configurator_views.xml', ], 'demo': [ 'data/sale_demo.xml', diff --git a/addons/sale/controllers/__init__.py b/addons/sale/controllers/__init__.py index ac5f1269f82..a35a880227c 100644 --- a/addons/sale/controllers/__init__.py +++ b/addons/sale/controllers/__init__.py @@ -3,4 +3,4 @@ from . import onboarding from . import portal -from . import product_configurator +from . import variant diff --git a/addons/sale/controllers/product_configurator.py b/addons/sale/controllers/product_configurator.py deleted file mode 100644 index 8ebcf879ba8..00000000000 --- a/addons/sale/controllers/product_configurator.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import http, fields -from odoo.http import request - - -class ProductConfiguratorController(http.Controller): - @http.route(['/product_configurator/configure'], type='json', auth="user", methods=['POST']) - def configure(self, product_id, pricelist_id, **kw): - add_qty = int(kw.get('add_qty', 1)) - product_template = request.env['product.template'].browse(int(product_id)) - to_currency = product_template.currency_id - pricelist = self._get_pricelist(pricelist_id) - - if pricelist: - product_template = product_template.with_context(pricelist=pricelist.id, partner=request.env.user.partner_id) - to_currency = pricelist.currency_id - - return request.env['ir.ui.view'].render_template("sale.product_configurator_configure", { - 'product': product_template, - # to_currency deprecated, get it from the pricelist or product directly - 'to_currency': to_currency, - 'pricelist': pricelist, - 'add_qty': add_qty, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions - }) - - @http.route(['/product_configurator/show_optional_products'], type='json', auth="user", methods=['POST']) - def show_optional_products(self, product_id, variant_values, pricelist_id, **kw): - pricelist = self._get_pricelist(pricelist_id) - return self._show_optional_products(product_id, variant_values, pricelist, False, **kw) - - @http.route(['/product_configurator/optional_product_items'], type='json', auth="user", methods=['POST']) - def optional_product_items(self, product_id, pricelist_id, **kw): - pricelist = self._get_pricelist(pricelist_id) - return self._optional_product_items(product_id, pricelist, **kw) - - @http.route(['/product_configurator/get_combination_info'], type='json', auth="user", methods=['POST']) - def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw): - combination = request.env['product.template.attribute.value'].browse(combination) - pricelist = self._get_pricelist(pricelist_id) - ProductTemplate = request.env['product.template'] - if 'context' in kw: - ProductTemplate = ProductTemplate.with_context(**kw.get('context')) - return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) - - @http.route(['/product_configurator/create_product_variant'], type='json', auth="user", methods=['POST']) - def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): - return request.env['product.template'].browse(int(product_template_id)).create_product_variant(product_template_attribute_value_ids) - - def _optional_product_items(self, product_id, pricelist, **kw): - add_qty = int(kw.get('add_qty', 1)) - 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 - - parent_combination = product.product_template_attribute_value_ids - if product.env.context.get('no_variant_attribute_values'): - # Add "no_variant" attribute values' exclusions - # They are kept in the context since they are not linked to this product variant - parent_combination |= product.env.context.get('no_variant_attribute_values') - - return request.env['ir.ui.view'].render_template("sale.optional_product_items", { - # product deprecated, it's not used in the view - 'product': product, - # reference_product deprecated, use parent_combination instead - 'reference_product': product, - 'parent_name': product.name, - 'parent_combination': parent_combination, - 'pricelist': pricelist, - # to_currency deprecated, get from pricelist or product - 'to_currency': to_currency, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions, - 'add_qty': add_qty, - }) - - def _show_optional_products(self, product_id, variant_values, pricelist, handle_stock, **kw): - product = request.env['product.product'].with_context(self._get_product_context(pricelist, **kw)).browse(int(product_id)) - combination = request.env['product.template.attribute.value'].browse(variant_values) - has_optional_products = product.optional_product_ids.filtered(lambda p: p._is_add_to_cart_possible(combination)) - - if not has_optional_products: - return False - - add_qty = int(kw.get('add_qty', 1)) - to_currency = (pricelist or product).currency_id - company = request.env['res.company'].browse(request.env.context.get('company_id')) or request.env['res.users']._get_company() - date = request.env.context.get('date') or fields.Date.today() - - def compute_currency(price): - return product.currency_id._convert(price, to_currency, company, date) - - no_variant_attribute_values = combination.filtered( - lambda product_template_attribute_value: product_template_attribute_value.attribute_id.create_variant == 'no_variant' - ) - if no_variant_attribute_values: - product = product.with_context(no_variant_attribute_values=no_variant_attribute_values) - - return request.env['ir.ui.view'].render_template("sale.optional_products_modal", { - 'product': product, - 'combination': combination, - 'add_qty': add_qty, - # reference_product deprecated, use combination instead - 'reference_product': product, - 'parent_name': product.name, - 'variant_values': variant_values, - 'pricelist': pricelist, - # compute_currency deprecated, get from pricelist or product - 'compute_currency': compute_currency, - # to_currency deprecated, get from pricelist or product - 'to_currency': to_currency, - 'handle_stock': handle_stock, - # get_attribute_exclusions deprecated, use product method - 'get_attribute_exclusions': self._get_attribute_exclusions, - }) - - def _get_attribute_exclusions(self, product, reference_product=None): - """deprecated, use product method""" - parent_combination = request.env['product.template.attribute.value'] - if reference_product: - parent_combination |= reference_product.product_template_attribute_value_ids - if reference_product.env.context.get('no_variant_attribute_values'): - # Add "no_variant" attribute values' exclusions - # They are kept in the context since they are not linked to this product variant - parent_combination |= reference_product.env.context.get('no_variant_attribute_values') - return product._get_attribute_exclusions(parent_combination, reference_product.name if reference_product else None) - - def _get_product_context(self, pricelist=None, **kw): - """deprecated, can be removed in master""" - product_context = dict(request.context) - if pricelist: - if not product_context.get('pricelist'): - product_context['pricelist'] = pricelist.id - product_context.update(kw.get('kwargs', {}).get('context', {})) - - return product_context - - def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw): - """deprecated, use product method""" - combination = request.env['product.template.attribute.value'].browse(combination) - return request.env['product.template'].browse(product_template_id)._get_combination_info(combination, product_id, add_qty, pricelist) - - def _get_pricelist(self, pricelist_id, pricelist_fallback=False): - return request.env['product.pricelist'].browse(int(pricelist_id or 0)) diff --git a/addons/sale/controllers/variant.py b/addons/sale/controllers/variant.py new file mode 100644 index 00000000000..3ca2c7c32b0 --- /dev/null +++ b/addons/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 + + +class VariantController(http.Controller): + @http.route(['/sale/get_combination_info'], type='json', auth="user", methods=['POST']) + def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw): + combination = request.env['product.template.attribute.value'].browse(combination) + pricelist = self._get_pricelist(pricelist_id) + ProductTemplate = request.env['product.template'] + if 'context' in kw: + ProductTemplate = ProductTemplate.with_context(**kw.get('context')) + return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + + @http.route(['/sale/create_product_variant'], type='json', auth="user", methods=['POST']) + def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): + return request.env['product.template'].browse(int(product_template_id)).create_product_variant(product_template_attribute_value_ids) + + def _get_pricelist(self, pricelist_id, pricelist_fallback=False): + return request.env['product.pricelist'].browse(int(pricelist_id or 0)) + + def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw): + """deprecated, use product method""" + combination = request.env['product.template.attribute.value'].browse(combination) + return request.env['product.template'].browse(product_template_id)._get_combination_info(combination, product_id, add_qty, pricelist) diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml index d0ccf291eff..d83063b9158 100644 --- a/addons/sale/data/sale_demo.xml +++ b/addons/sale/data/sale_demo.xml @@ -730,23 +730,6 @@ Thanks! - - Chair floor protection - - 12.0 - 0.01 - - - Office chairs can harm your floor: protect it. - - - - - - - - - Custom diff --git a/addons/sale/models/product_product.py b/addons/sale/models/product_product.py index a489b03b952..8d919e39ac3 100644 --- a/addons/sale/models/product_product.py +++ b/addons/sale/models/product_product.py @@ -5,16 +5,6 @@ from odoo import api, fields, models from odoo.tools.float_utils import float_round -class ProductTemplate(models.Model): - _inherit = "product.template" - - optional_product_ids = fields.Many2many( - 'product.template', 'product_optional_rel', 'src_id', 'dest_id', - string='Optional Products', help="Optional Products are suggested " - "whenever the customer hits *Add to Cart* (cross-sell strategy, " - "e.g. for computers: warranty, software, etc.).") - - class ProductProduct(models.Model): _inherit = 'product.product' diff --git a/addons/sale/static/src/js/product_configurator_controller.js b/addons/sale/static/src/js/product_configurator_controller.js deleted file mode 100644 index 7ca3d32c292..00000000000 --- a/addons/sale/static/src/js/product_configurator_controller.js +++ /dev/null @@ -1,199 +0,0 @@ -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' - }), - - /** - * @override - */ - start: function () { - this.$el.addClass('o_product_configurator'); - return 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 awa: Find a better way to catch that event. - * - * @override - */ - _onFieldChanged: function (event) { - this._super.apply(this, arguments); - - var self = this; - var product_id = event.data.changes.product_template_id.id; - - // check to prevent traceback when emptying the field - if (!product_id) { - return; - } - - this.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled'); - - this._rpc({ - route: '/product_configurator/configure', - params: { - product_id: product_id, - pricelist_id: this.renderer.pricelistId - } - }).then(function (configurator) { - self.renderer.renderConfigurator(configurator); - }); - }, - - //-------------------------------------------------------------------------- - // 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. - * - * This will also create the product if it's in "dynamic" mode - * (see product_attribute.create_variant) - * - * @private - * @param {$.Element} $modal - */ - _handleAdd: function ($modal) { - var self = this; - var productSelector = [ - 'input[type="hidden"][name="product_id"]', - 'input[type="radio"][name="product_id"]:checked' - ]; - - var productId = parseInt($modal.find(productSelector.join(', ')).first().val(), 10); - this.renderer.selectOrCreateProduct( - $modal, - productId, - $modal.find('.product_template_id').val(), - false - ).then(function (productId) { - $modal.find(productSelector.join(', ')).val(productId); - - var variantValues = self - .renderer - .getSelectedVariantValues($modal.find('.js_product')); - - var productCustomVariantValues = self - .renderer - .getCustomVariantValues($modal.find('.js_product')); - - var noVariantAttributeValues = self - .renderer - .getNoVariantAttributeValues($modal.find('.js_product')); - - self.rootProduct = { - product_id: productId, - quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1), - variant_values: variantValues, - product_custom_attribute_values: productCustomVariantValues, - no_variant_attribute_values: noVariantAttributeValues - }; - - self.optionalProductsModal = new OptionalProductsModal($('body'), { - rootProduct: self.rootProduct, - pricelistId: self.renderer.pricelistId, - okButtonText: _t('Confirm'), - cancelButtonText: _t('Back'), - title: _t('Configure'), - context: self.initialState.context, - }).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)); - }); - }, - - /** - * 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()); - }, - - /** - * Update product configurator form - * when quantity is updated in the optional products window - * - * @private - * @param {integer} quantity - */ - _onOptionsUpdateQuantity: function (quantity) { - this.$el - .find('input[name="add_qty"]') - .val(quantity) - .trigger('change'); - }, - - /** - * This triggers the close action for the window and - * adds the product as the "infos" parameter. - * It will allow the caller (typically the SO line form) 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 - * {Array} products.product_custom_attribute_values: - * see product_configurator_mixin.getCustomVariantValues - * {Array} products.no_variant_attribute_values: - * see product_configurator_mixin.getNoVariantAttributeValues - */ - _addProducts: function (products) { - this.do_action({type: 'ir.actions.act_window_close', infos: products}); - } -}); - -return ProductConfiguratorFormController; - -}); \ No newline at end of file diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 80471814207..7e7847215a7 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -68,7 +68,7 @@ var ProductConfiguratorMixin = { self._checkExclusions($parent, combination); - ajax.jsonRpc(this._getUri('/product_configurator/get_combination_info'), 'call', { + ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', { product_template_id: parseInt($parent.find('.product_template_id').val()), product_id: this._getProductId($parent), combination: combination, @@ -311,7 +311,7 @@ var ProductConfiguratorMixin = { // We don't handle that compatibility because the previous code was // not working either: it was making an RPC that failed with any // non-admin user anyway. To use this feature, restart the server. - var route = '/product_configurator/create_product_variant'; + var route = '/sale/create_product_variant'; if (useAjax) { productReady = ajax.jsonRpc(route, 'call', params); } else { diff --git a/addons/sale/static/src/js/product_configurator_modal.js b/addons/sale/static/src/js/product_configurator_modal.js deleted file mode 100644 index 3bcebf88725..00000000000 --- a/addons/sale/static/src/js/product_configurator_modal.js +++ /dev/null @@ -1,483 +0,0 @@ -odoo.define('sale.OptionalProductsModal', function (require) { - "use strict"; - -var ajax = require('web.ajax'); -var Dialog = require('web.Dialog'); -var ServicesMixin = require('web.ServicesMixin'); -var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin'); - -var productNameMap = {}; -var optionalProductsMap = {}; - -var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixin, { - events: _.extend({}, Dialog.prototype.events, ProductConfiguratorMixin.events, { - 'click a.js_add, a.js_remove': '_onAddOrRemoveOption', - 'change .in_cart.main_product input.js_quantity': '_onChangeQuantity' - }), - /** - * Initializes the optional products modal - * - * If the "isWebsite" param is true, will also disable the following events: - * - change [data-attribute_exclusions] - * - click button.js_add_cart_json - * - * This has to be done because those events are already registered at the "website_sale" - * component level. - * This modal is part of the form that has these events registered and we - * want to avoid duplicates. - * - * @override - * @param {$.Element} parent The parent container - * @param {Object} params - * @param {integer} params.pricelistId - * @param {boolean} params.isWebsite If we're on a web shop page, we need some - * custom behavior - * @param {string} params.okButtonText The text to apply on the "ok" button, typically - * "Add" for the sale order and "Proceed to checkout" on the web shop - * @param {string} params.cancelButtonText same as "params.okButtonText" but - * for the cancel button - * @param {Object} params.rootProduct The root product of the optional products window - * @param {integer} params.rootProduct.product_id - * @param {integer} params.rootProduct.quantity - * @param {Array} params.rootProduct.variant_values - * @param {Array} params.rootProduct.product_custom_attribute_values - * @param {Array} params.rootProduct.no_variant_attribute_values - */ - init: function (parent, params) { - var self = this; - - var options = _.extend({ - size: 'large', - buttons: [{ - text: params.okButtonText, - click: this._onConfirmButtonClick, - classes: 'btn-primary' - }, { - text: params.cancelButtonText, - click: this._onCancelButtonClick - }], - technical: !params.isWebsite, - }, params || {}); - - this._super(parent, options); - - this.context = params.context; - 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' : ''); - - if (this.isWebsite) { - delete this.events['click button.js_add_cart_json']; - } - this._productImageField = 'image_medium'; - }, - /** - * @override - */ - willStart: function () { - var self = this; - - var uri = this._getUri("/product_configurator/show_optional_products"); - var getModalContent = ajax.jsonRpc(uri, 'call', { - product_id: self.rootProduct.product_id, - variant_values: self.rootProduct.variant_values, - pricelist_id: self.pricelistId || false, - add_qty: self.rootProduct.quantity, - kwargs: { - context: _.extend({ - 'quantity': self.rootProduct.quantity - }, this.context), - } - }) - .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 Promise.all([getModalContent, parentInit]); - }, - - /** - * This is overridden to append the modal to the provided container (see init("parent")). - * We need this to have the modal contained in the web shop product form. - * The additional products data will then be contained in the form and sent on submit. - * - * @override - */ - open: function (options) { - $('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened - - var self = this; - this.appendTo($('
')).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.$modal.focus(); - self._openedResolver(); - } - }); - if (options && options.shouldFocusButtons) { - self._onFocusControlButton(); - } - - return self; - }, - /** - * Will update quantity input to synchronize with previous window - * - * @override - */ - start: function () { - var def = this._super.apply(this, arguments); - var self = this; - - this.$el.find('input[name="add_qty"]').val(this.rootProduct.quantity); - - return def.then(function () { - // This has to be triggered to compute the "out of stock" feature - self._opened.then(function () { - self.triggerVariantChange(self.$el); - }); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Returns the list of selected products. - * The root product is added on top of the list. - * - * @returns {Array} products - * {integer} product_id - * {integer} quantity - * {Array} product_custom_variant_values - * {Array} no_variant_attribute_values - * @public - */ - getSelectedProducts: function () { - var self = this; - 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){ - quantity = $item.find('input[name="add_qty"]').val(); - } else { - quantity = parseInt( - $item - .find('.optional_product_quantity span.add_qty') - .html() - .trim() - ); - } - - var productCustomVariantValues = self.getCustomVariantValues($(this)); - var noVariantAttributeValues = self.getNoVariantAttributeValues($(this)); - products.push({ - product_id: parseInt($item.find('input.product_id').val()), - quantity: quantity, - product_custom_attribute_values: productCustomVariantValues, - no_variant_attribute_values: noVariantAttributeValues - }); - }); - - return products; - }, - - // ------------------------------------------ - // Private - // ------------------------------------------ - - /** - * Adds the product image and updates the product description - * based on attribute values that are either "no variant" or "custom". - * - * @private - */ - _postProcessContent: function ($modalContent) { - var productId = this.rootProduct.product_id; - $modalContent - .find('img:first') - .attr("src", "/web/image/product.product/" + productId + "/image_medium"); - - if (this.rootProduct && - (this.rootProduct.product_custom_attribute_values || - this.rootProduct.no_variant_attribute_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_attribute_values, function (){ - description += '
' + this.attribute_value_name + ': ' + this.custom_value; - }); - - $.each(this.rootProduct.no_variant_attribute_values, function (){ - if (this.is_custom !== 'True'){ - description += '
' + this.attribute_name + ': ' + this.attribute_value_name; - } - }); - - $productDescription.html(description); - } - - return $modalContent; - }, - - /** - * @private - */ - _onConfirmButtonClick: function () { - this.trigger('confirm'); - this.close(); - }, - - /** - * @private - */ - _onCancelButtonClick: function () { - this.trigger('back'); - this.close(); - }, - - /** - * Will add/remove the option, that includes: - * - Moving it to the correct DOM section - * and possibly under its parent product - * - Hiding attribute values selection and showing the quantity - * - Creating the product if it's in "dynamic" mode (see product_attribute.create_variant) - * - Updating the description based on custom/no_create attribute values - * - Removing optional products if parent product is removed - * - Computing the total price - * - * @private - * @param {MouseEvent} ev - */ - _onAddOrRemoveOption: function (ev){ - ev.preventDefault(); - var self = this; - var $target = $(ev.currentTarget); - var $modal = $target.parents('.oe_optional_products_modal'); - 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($target.hasClass("js_add") ? 1 : 0 ); - $parent.find(".js_remove"); - - var productTemplateId = $parent.find(".product_template_id").val(); - if ($target.hasClass('js_add')) { - self._onAddOption($modal, $parent ,productTemplateId); - } else { - self._onRemoveOption($modal, $parent ,productTemplateId); - } - - self.computePriceTotal(); - }, - - /** - * @private - * @see _onAddOrRemoveOption - * @param {$.Element} $modal - * @param {$.Element} $parent - * @param {integer} productTemplateId - */ - _onAddOption: function ($modal, $parent, productTemplateId) { - var self = this; - var $main_product = $modal.find('.js_product:first'); - var $selectOptionsText = $modal.find('.o_select_options'); - - // 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'); - $parent.find('.td-qty').append($parent.find('.optional_product_quantity')); - - // change product name to display_name (containing attribute values) when it's added to cart - // the "simple" name is kept into a map to revert to it if the product is removed from the cart - productNameMap[productTemplateId] = $parent.find(".product-name").html(); - $parent.find(".product-name").html($parent.find(".product_display_name").val()); - - var productCustomVariantValues = self.getCustomVariantValues($parent); - var noVariantAttributeValues = self.getNoVariantAttributeValues($parent); - if (productCustomVariantValues || noVariantAttributeValues) { - var $productDescription = $parent - .find('td.td-product_name div.float-left'); - - var description = ''; - $.each(productCustomVariantValues, function (){ - description += '
' + this.attribute_value_name + ': ' + this.custom_value; - }); - - $.each(noVariantAttributeValues, function (){ - if (this.is_custom !== 'True'){ - description += '
' + this.attribute_name + ': ' + this.attribute_value_name; - } - }); - - var $customAttributeValuesDescription = $('
', { - 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 parentProductId = null; - for (var productIdKey in optionalProductsMap) { - if (optionalProductsMap[productIdKey].indexOf(productTemplateId) !== -1) { - parentProductId = productIdKey; - break; - } - - $('tr:last').after($parent); - } - - if (parentProductId) { - $modal.find('.product_template_id').filter(function () { - return parentProductId === $(this).val(); - }).parents('.js_product:first').after($parent); - } else { - // else, place it after the main product - $main_product.after($parent); - } - - this.selectOrCreateProduct( - $parent, - $parent.find('.product_id').val(), - productTemplateId, - true - ).then(function (productId) { - $parent.find('.product_id').val(productId); - - ajax.jsonRpc(self._getUri("/product_configurator/optional_product_items"), 'call', { - 'product_id': productId, - 'pricelist_id': self.pricelistId || false, - }).then(function (addedItem) { - var $addedItem = $(addedItem); - $modal.find('tr:last').after($addedItem); - - self.$el.find('input[name="add_qty"]').trigger('change'); - self.triggerVariantChange($addedItem); - - if ($addedItem.find(".product_template_id").length > 0) { - // we need to map this product with it's optional products to be able to: - // - remove them from the cart if the parent product is removed - // - place the optional products under their parent in - // the interface when they're added to the cart - optionalProductsMap[productTemplateId] = $addedItem - .find(".product_template_id") - .map(function () { - return $(this).val(); - }).get(); - } - - if ($selectOptionsText.nextAll('.js_product').length === 0){ - // no more optional products to select -> hide the header - $selectOptionsText.hide(); - } - }); - }); - }, - - /** - * @private - * @see _onAddOrRemoveOption - * @param {$.Element} $modal - * @param {$.Element} $parent - * @param {integer} productTemplateId - */ - _onRemoveOption: function ($modal, $parent, productTemplateId){ - // restore attribute values selection - $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(); - - var $select_options_text = $modal.find('.o_select_options'); - $select_options_text.show(); - - // revert back to original product name and clean the entry - $parent.find(".product-name").html(productNameMap[productTemplateId]); - delete productNameMap[productTemplateId]; - - this._removeOptionOption($modal, productTemplateId); - - $modal.find('tr:last').after($parent); - }, - - /** - * If the removed product had optional products, remove them as well - * - * @private - * @param {$.Element} $modal - * @param {integer} optionId The removed optional product id - */ - _removeOptionOption: function ($modal, optionId) { - var self = this; - if (optionalProductsMap[optionId]) { - optionalProductsMap[optionId].forEach(function (productId) { - $modal.find('.product_template_id').filter(function () { - return productId === $(this).val(); - }).parents('.js_product:first').remove(); - - self._removeOptionOption($modal, productId); - }); - - delete optionalProductsMap[optionId]; - } - }, - /** - * @override - */ - _onChangeCombination:function (ev, $parent, combination) { - $parent - .find('.td-product_name .product-name') - .first() - .text(combination.display_name); - - ProductConfiguratorMixin._onChangeCombination.apply(this, arguments); - }, - /** - * When the quantity of the root product is updated, we need to update - * the quantity of all the selected optional products. - * - * @private - * @param {MouseEvent} ev - */ - _onChangeQuantity: function (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'); - } else { - $modal.find(".js_items").removeClass('d-none').addClass('add_qty').text(qty); - $modal.find(".js_item").addClass('d-none').removeClass('add_qty'); - } - - this.rootProduct.quantity = qty; - - if (!this.isWebsite){ - this.onChangeAddQuantity(ev); - } - - this.trigger('update_quantity', qty); - } -}); - -return OptionalProductsModal; - -}); \ No newline at end of file diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale/static/src/js/product_configurator_renderer.js deleted file mode 100644 index f3f437461ea..00000000000 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ /dev/null @@ -1,54 +0,0 @@ -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 = this.state.context.default_pricelist_id || 0; - }, - /** - * @override - */ - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - self.$el.append($('
', {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; - -}); diff --git a/addons/sale/static/src/js/product_configurator_view.js b/addons/sale/static/src/js/product_configurator_view.js deleted file mode 100644 index 5a3bb4401be..00000000000 --- a/addons/sale/static/src/js/product_configurator_view.js +++ /dev/null @@ -1,20 +0,0 @@ -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; - -}); \ No newline at end of file diff --git a/addons/sale/static/tests/product_configurator.test.js b/addons/sale/static/tests/product_configurator.test.js deleted file mode 100644 index c751eb6c111..00000000000 --- a/addons/sale/static/tests/product_configurator.test.js +++ /dev/null @@ -1,172 +0,0 @@ -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 createView = testUtils.createView; - - var getArch = function (){ - return '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
'; - }; - - QUnit.module('Product Configurator', { - beforeEach: function () { - this.data = { - product_template: { - fields: { - id: {type: 'integer'} - }, - records: [{ - id: 42, - display_name: "Customizable Desk" - }] - }, - product: { - fields: { - id: {type: 'integer'} - }, - records: [{ - id: 1, - display_name: "Customizable Desk (1)" - }, { - id: 2, - display_name: "Customizable Desk (2)" - }] - }, - sale_order: { - fields: { - id: {type: 'integer'}, - pricelist_id: { - string: 'Pricelist', - type: 'one2many', - relation: 'pricelist' - }, - sale_order_line: { - string: 'lines', - type: 'one2many', - relation: 'sale_order_line' - }, - } - }, - sale_order_line: { - fields: { - product_id: { - string: 'product', - type: 'many2one', - relation: 'product' - }, - product_uom_qty: {type: 'integer'} - } - }, - sale_product_configurator: { - fields: { - product_template_id: { - string: 'product', - type: 'many2one', - relation: 'product_template' - } - }, - records: [{ - product_template_id: 42 - }] - }, - pricelist: { - fields: { - id: {type: 'integer'} - } - } - }; - }, - }, function (){ - QUnit.test('click on "Configure a product" and check for form loading', async function (assert) { - assert.expect(2); - - var form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: getArch(), - mockRPC: function (route) { - if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - assert.ok(true); - return Promise.resolve(1); - } - return this._super.apply(this, arguments); - }, - }); - - assert.containsOnce(form, "a:contains('Configure a product')"); - - await testUtils.dom.click(form.$("a:contains('Configure a product')")); - form.destroy(); - }); - - QUnit.test('trigger_up the "add_record" event and checks that rows are correctly added to the list', async function (assert) { - assert.expect(1); - - var form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: getArch(), - }); - - var list = form.renderer.allFieldWidgets[form.handle][1]; - - list.trigger_up('add_record', { - context: [{default_product_id: 1, default_product_uom_qty: 2}, {default_product_id: 2, default_product_uom_qty: 3}], - forceEditable: "bottom" , - allowWarning: true - }); - await testUtils.nextTick(); - - assert.containsN(list, "tr.o_data_row", 2); - form.destroy(); - }); - - QUnit.test('Select a product in the list and check for template loading', async function (assert){ - assert.expect(1); - - var product_configurator_form = await createView({ - View: ProductConfiguratorFormView, - model: 'sale_product_configurator', - data: this.data, - arch: - '
' + - '' + - '' + - '' + - '
' + - '
' + - '
', - mockRPC: function (route) { - if (route === '/product_configurator/configure') { - assert.ok(true); - return Promise.resolve('
plop
'); - } - return this._super.apply(this, arguments); - } - }); - await testUtils.dom.click(product_configurator_form.$('.o_input')); - await testUtils.dom.click($("ul.ui-autocomplete li a:contains('Customizable Desk')").mouseenter()); - product_configurator_form.destroy(); - }); - }); -}); diff --git a/addons/sale/views/assets.xml b/addons/sale/views/assets.xml index ea5c82b1c86..9e7e42eff80 100644 --- a/addons/sale/views/assets.xml +++ b/addons/sale/views/assets.xml @@ -8,11 +8,6 @@ - - - - - @@ -28,7 +23,6 @@ diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index bc1366e5d5c..3f2bb403453 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -18,7 +18,7 @@
+
+
+ +
+
+
+
diff --git a/addons/sale/views/sale_product_configurator_templates.xml b/addons/sale/views/sale_product_configurator_templates.xml deleted file mode 100644 index 1f0ec29d534..00000000000 --- a/addons/sale/views/sale_product_configurator_templates.xml +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index bcf728e356b..e793650ecb6 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -81,13 +81,6 @@ 0 - - - - - - - diff --git a/addons/sale/views/variant_templates.xml b/addons/sale/views/variant_templates.xml new file mode 100644 index 00000000000..8d52d860be4 --- /dev/null +++ b/addons/sale/views/variant_templates.xml @@ -0,0 +1,108 @@ + + + + diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py index 145cce89c1e..09f069f85ed 100644 --- a/addons/sale/wizard/__init__.py +++ b/addons/sale/wizard/__init__.py @@ -3,4 +3,3 @@ from . import payment_acquirer_onboarding_wizard from . import sale_make_invoice_advance -from . import sale_product_configurator \ No newline at end of file diff --git a/addons/sale/wizard/sale_product_configurator.py b/addons/sale/wizard/sale_product_configurator.py deleted file mode 100644 index 75563872928..00000000000 --- a/addons/sale/wizard/sale_product_configurator.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields - - -class SaleProductConfigurator(models.TransientModel): - _name = 'sale.product.configurator' - _description = 'Sale Product Configurator' - - product_template_id = fields.Many2one( - 'product.template', string="Product", - required=True, domain=[('sale_ok', '=', True), '|', ('attribute_line_ids.value_ids', '!=', False), ('optional_product_ids', '!=', False)]) - pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True) diff --git a/addons/sale/wizard/sale_product_configurator_views.xml b/addons/sale/wizard/sale_product_configurator_views.xml deleted file mode 100644 index 9b29c3f9183..00000000000 --- a/addons/sale/wizard/sale_product_configurator_views.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - sale.product.configurator.view.form - sale.product.configurator - -
- - - -
-
-
-
-
-
\ No newline at end of file diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py index 3bf801a604b..98f77d20956 100644 --- a/addons/sale_management/__manifest__.py +++ b/addons/sale_management/__manifest__.py @@ -47,7 +47,6 @@ The Dashboard for the Sales Manager will include 'views/sale_management_views.xml', 'views/digest_views.xml', 'views/sale_order_views.xml', - 'views/assets.xml', 'report/sale_report_templates.xml', ], 'demo': [ diff --git a/addons/sale_management/static/tests/product_configurator_advanced_test_ui.js b/addons/sale_management/static/tests/product_configurator_advanced_test_ui.js deleted file mode 100644 index 074da95f06e..00000000000 --- a/addons/sale_management/static/tests/product_configurator_advanced_test_ui.js +++ /dev/null @@ -1,160 +0,0 @@ -odoo.define('sale.sale_product_configurator_advanced_tour', function (require) { -"use strict"; - -var tour = require('web_tour.tour'); - -var optionVariantImage; - -tour.register('sale_product_configurator_advanced_tour', { - url: "/web", - test: true, -}, [tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'community' -}, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'enterprise' -}, { - trigger: ".o_list_button_add", - extra_trigger: ".o_sale_order" -}, { - trigger: ".o_required_modifier[name=partner_id] input", - run: "text Tajine Saucisse", -}, { - trigger: ".ui-menu-item > a:contains('Tajine Saucisse')", - auto: true, -}, { - trigger: "a:contains('Configure a product')", - extra_trigger: ".o_field_widget[name=pricelist_id] > .o_external_button", // Wait for pricelist (onchange_partner_id) -}, { - 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: 'span:contains("Custom")', - extra_trigger: '.o_product_configurator', - run: 'click' -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(1) .variant_custom_value', - extra_trigger: '.o_product_configurator', - run: 'text Custom 1' -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(3) span:contains("PAV9")', - extra_trigger: '.o_product_configurator', - run: 'click' -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(3) .variant_custom_value', - extra_trigger: '.o_product_configurator', - run: 'text Custom 2' -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(4) span:contains("PAV5")', - extra_trigger: '.o_product_configurator', - run: 'click' -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select ', - extra_trigger: '.o_product_configurator', - run: function (){ - var inputValue = $('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) option[data-value_name="PAV9"]').val(); - $('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select').val(inputValue); - $('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select').trigger('change'); - } -}, { - trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) .variant_custom_value', - extra_trigger: '.o_product_configurator', - run: 'text Custom 3' -}, { - trigger: ".o_sale_product_configurator_add", - run: 'click' -}, { - trigger: '.main_product strong:contains("Custom, White, PAV9, PAV5, PAV1")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("Custom: Custom 1")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("PAV9: Custom 2")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("PAV9: Custom 3")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("PA5: PAV1")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("PA5: PAV1")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.main_product div:contains("PA8: PAV1")', - extra_trigger: '.oe_optional_products_modal', - run: function () {} //check -}, { - trigger: '.oe_optional_products_modal .js_product:eq(1) div:contains("Conference Chair (Steel)")', - run: function () { - optionVariantImage = $('.oe_optional_products_modal .js_product:eq(1) img.variant_image').attr('src'); - } -}, { - trigger: '.oe_optional_products_modal .js_product:eq(1) input[data-value_name="Aluminium"]', -}, { - trigger: '.oe_optional_products_modal .js_product:eq(1) div:contains("Conference Chair (Aluminium)")', - run: function () { - var newVariantImage = $('.oe_optional_products_modal .js_product:eq(1) img.variant_image').attr('src'); - if (newVariantImage !== optionVariantImage) { - $('

').text('image variant option src changed').insertAfter('.oe_optional_products_modal .js_product:eq(1) .product-name'); - } - - } -}, { - extra_trigger: '.oe_optional_products_modal .js_product:eq(1) div:contains("image variant option src changed")', - trigger: '.oe_optional_products_modal .js_product:eq(1) input[data-value_name="Steel"]', -}, { - trigger: 'button span:contains(Confirm)', - extra_trigger: '.oe_optional_products_modal', - run: 'click' -}, { - trigger: 'td.o_data_cell:contains("Customizable Desk (Custom, White, PAV9, PAV5, PAV1)")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("Custom: Custom 1")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("PAV9: Custom 2")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("PAV9: Custom 3")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("PA5: PAV1")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("PA5: PAV1")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}, { - trigger: 'td.o_data_cell:contains("PA8: PAV1")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} //check -}]); - -}); diff --git a/addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js b/addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js deleted file mode 100644 index 0cb9b46e530..00000000000 --- a/addons/sale_management/static/tests/product_configurator_pricelist_test_ui.js +++ /dev/null @@ -1,100 +0,0 @@ -odoo.define('sale.product_configurator_pricelist_tour', function (require) { -"use strict"; - -var tour = require('web_tour.tour'); - -tour.register('sale_product_configurator_pricelist_tour', { - url: "/web", - test: true, -}, -[ -tour.STEPS.SHOW_APPS_MENU_ITEM, -{ - content: "navigate to the sale app", - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'community' -}, { - content: "navigate to the sale app", - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'enterprise' -}, { - content: "create a new order", - trigger: '.o_list_button_add', - extra_trigger: ".o_sale_order" -}, { - content: "search the partner", - trigger: 'div[name="partner_id"] input', - run: 'text Azure' -}, { - content: "select the partner", - trigger: 'ul.ui-autocomplete > li > a:contains(Azure)', -}, { - content: "search the pricelist", - trigger: 'div[name="pricelist_id"] input', - run: 'text Public Pricelist' -}, { - content: "select the pricelist", - trigger: 'ul.ui-autocomplete > li > a:contains(Public Pricelist)', -}, { - content: "wait for pricelist to be set", - trigger: 'div.o_with_button[name="pricelist_id"]', - run: function () {}, -}, { - content: "open product configurator", - trigger: 'a:contains("Configure a product")', -}, { - content: "open product selector", - trigger: '.o_product_configurator .o_input_dropdown input', - run: 'click' -}, { - content: "select desk", - trigger: 'li a:contains("Customizable Desk")', - in_modal: false, - extra_trigger: 'ul.ui-autocomplete', -}, { - content: "check price is correct (USD)", - trigger: 'span.oe_currency_value:contains("750.00")', - run: function () {} // check price -}, { - content: "add one more", - trigger: 'button.js_add_cart_json:has(i.fa-plus)', -}, { - content: "check price for 2", - trigger: 'span.oe_currency_value:contains("600.00")', - run: function () {} // check price (pricelist has discount for 2) -}, { - content: "click add", - trigger: '.o_sale_product_configurator_add:not(.disabled)' -}, { - content: "check we are on the add modal", - trigger: '.td-product_name:contains("Customizable Desk (Steel, White)")', - extra_trigger: '.oe_optional_products_modal', - run: 'click' -}, { - content: "add conference chair", - trigger: '.js_product:has(strong:contains(Conference Chair)) .js_add', - extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair))', - run: 'click' -}, { - content: "add chair floor protection", - trigger: '.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' -}, { - content: "verify configurator final price", // tax excluded - trigger: '.o_total_row .oe_currency_value:contains("1,257.00")', -}, { - content: "add to SO", - trigger: 'button span:contains(Confirm)', - extra_trigger: '.oe_optional_products_modal', - run: 'click' -}, { - content: "verify SO final price excluded", - trigger: 'span[name="amount_untaxed"]:contains("1,257.00")', -}, { - content: "verify SO final price included", - trigger: 'span[name="amount_total"]:contains("1,437.00")', -} -]); - -}); diff --git a/addons/sale_management/static/tests/product_configurator_test_ui.js b/addons/sale_management/static/tests/product_configurator_test_ui.js deleted file mode 100644 index 03f5a0a8bbc..00000000000 --- a/addons/sale_management/static/tests/product_configurator_test_ui.js +++ /dev/null @@ -1,99 +0,0 @@ -odoo.define('sale.product_configurator_tour', function (require) { -"use strict"; - -var tour = require('web_tour.tour'); - -// Note: please keep this test without pricelist for maximum coverage. -// The pricelist is tested on the other tours. - -tour.register('sale_product_configurator_tour', { - url: "/web", - test: true, -}, [tour.STEPS.SHOW_APPS_MENU_ITEM, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'community' -}, { - trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]', - edition: 'enterprise' -}, { - 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("Steel")', - run: function () { - $('input.product_id').change(function () { - var request_count = 0; - if ($('.o_sale_product_configurator_add').attr('request_count')) { - request_count = parseInt($('.o_sale_product_configurator_add').attr('request_count')); - } - request_count++; - $('.o_sale_product_configurator_add').attr('request_count', request_count); - }); - } -}, { - trigger: '.configurator_container span:contains("Aluminium")', - run: 'click' -}, { - trigger: 'span.oe_currency_value:contains("800.40")', - run: function (){} // check updated price -}, { - trigger: 'input[data-value_name="Black"]' -}, { - trigger: 'input[data-value_name="White"]' -}, { - trigger: '.o_sale_product_configurator_add[request_count="3"]', - run: function (){} // used to sync with "get_combination_info" completion -}, { - trigger: '.o_sale_product_configurator_add:not(.disabled)' -}, { - trigger: 'span:contains("Aluminium")', - extra_trigger: '.oe_optional_products_modal', - run: 'click' -}, { - trigger: '.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: '.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: 'button span:contains(Confirm)', - extra_trigger: '.oe_optional_products_modal', - id: "quotation_product_selected", - run: 'click' -}, -// check that 3 products were added to the SO -{ - trigger: 'td.o_data_cell:contains("Customizable Desk (Aluminium, White)")', - extra_trigger: 'div[name="order_line"]', - in_modal: false, - run: function (){} -}, { - trigger: 'td.o_data_cell:contains("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 (){} -}, { - trigger: '.o_readonly_modifier[name=amount_total]:contains("0.00")', - in_modal: false, - run: function (){} -} -]); - -}); diff --git a/addons/sale_management/tests/test_sale_ui.py b/addons/sale_management/tests/test_sale_ui.py index 6ec2f4167c1..cf107b02ebc 100644 --- a/addons/sale_management/tests/test_sale_ui.py +++ b/addons/sale_management/tests/test_sale_ui.py @@ -7,119 +7,3 @@ class TestUi(odoo.tests.HttpCase): def test_01_sale_tour(self): self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_tour.ready", login="admin") - - def test_02_product_configurator(self): - # group_product_variant: use the product configurator - # group_sale_pricelist: display the pricelist to determine when it is changed after choosing - # the partner - self.env.ref('base.user_admin').write({ - 'groups_id': [ - (4, self.env.ref('product.group_product_variant').id), - (4, self.env.ref('product.group_sale_pricelist').id), - ], - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_tour.ready", login="admin") - - def test_03_product_configurator_advanced(self): - # group_product_variant: use the product configurator - # group_sale_pricelist: display the pricelist to determine when it is changed after choosing - # the partner - self.env.ref('base.user_admin').write({ - 'groups_id': [ - (4, self.env.ref('product.group_product_variant').id), - (4, self.env.ref('product.group_sale_pricelist').id), - ], - }) - - # Prepare relevant test data - # This is not included in demo data to avoid useless noise - product_attributes = self.env['product.attribute'].create([{ - 'name': 'PA1', - 'type': 'radio', - 'create_variant': 'dynamic' - }, { - 'name': 'PA2', - 'type': 'radio', - 'create_variant': 'always' - }, { - 'name': 'PA3', - 'type': 'radio', - 'create_variant': 'dynamic' - }, { - 'name': 'PA4', - 'type': 'select', - 'create_variant': 'no_variant' - }, { - 'name': 'PA5', - 'type': 'select', - 'create_variant': 'no_variant' - }, { - 'name': 'PA7', - 'type': 'color', - 'create_variant': 'no_variant' - }, { - 'name': 'PA8', - 'type': 'radio', - 'create_variant': 'no_variant' - }]) - - product_attribute_values = self.env['product.attribute.value'].create([{ - 'name': 'PAV' + str(i), - 'is_custom': i == 9, - 'attribute_id': product_attribute.id - } for i in range(1, 11) for product_attribute in product_attributes]) - - product_template_attribute_lines = self.env['product.template.attribute.line'].create([{ - 'attribute_id': product_attribute.id, - 'product_tmpl_id': self.env.ref("product.product_product_4").id, - 'value_ids': [(6, 0, product_attribute_values.filtered( - lambda product_attribute_value: product_attribute_value.attribute_id == product_attribute - ).ids)] - } for product_attribute in product_attributes]) - - self.env.ref("product.product_product_4").update({ - 'attribute_line_ids': [(4, product_template_attribute_line.id) for product_template_attribute_line in product_template_attribute_lines] - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_advanced_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_advanced_tour.ready", login="admin") - - def test_04_product_configurator_pricelist(self): - """The goal of this test is to make sure pricelist rules are correctly - applied on the backend product configurator. - Also testing B2C setting: no impact on the backend configurator. - """ - - admin = self.env.ref('base.user_admin') - - # Activate B2C - self.env.ref('account.group_show_line_subtotals_tax_included').users |= admin - self.env.ref('account.group_show_line_subtotals_tax_excluded').users -= admin - - # Active pricelist on SO - self.env.ref('product.group_sale_pricelist').users |= admin - - # Add a 15% tax on desk - tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 15}) - self.env.ref('product.product_product_4_product_template').taxes_id = tax - - # Remove tax from Conference Chair and Chair floor protection - self.env.ref('sale.product_product_1_product_template').taxes_id = None - self.env.ref('product.product_product_11_product_template').taxes_id = None - - # Make sure pricelist rule exist - product_template = self.env.ref('product.product_product_4_product_template') - pricelist = self.env.ref('product.list0') - - if not pricelist.item_ids.filtered(lambda i: i.product_tmpl_id == product_template and i.price_discount == 20): - self.env['product.pricelist.item'].create({ - 'base': 'list_price', - 'applied_on': '1_product', - 'pricelist_id': pricelist.id, - 'product_tmpl_id': product_template.id, - 'price_discount': 20, - 'min_quantity': 2, - 'compute_price': 'formula', - }) - - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_pricelist_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_pricelist_tour.ready", login="admin") diff --git a/addons/sale_management/views/assets.xml b/addons/sale_management/views/assets.xml deleted file mode 100644 index a5a3134949d..00000000000 --- a/addons/sale_management/views/assets.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - + diff --git a/addons/sale_product_configurator/views/sale_views.xml b/addons/sale_product_configurator/views/sale_views.xml new file mode 100644 index 00000000000..499cb2c7eb0 --- /dev/null +++ b/addons/sale_product_configurator/views/sale_views.xml @@ -0,0 +1,17 @@ + + + + product.template.form.inherit.sale.product.configurator + product.template + + + + + + + + + + + + diff --git a/addons/sale_product_configurator/views/templates.xml b/addons/sale_product_configurator/views/templates.xml new file mode 100644 index 00000000000..534a199dc70 --- /dev/null +++ b/addons/sale_product_configurator/views/templates.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + diff --git a/addons/sale_product_configurator/wizard/sale_product_configurator.py b/addons/sale_product_configurator/wizard/sale_product_configurator.py new file mode 100644 index 00000000000..75563872928 --- /dev/null +++ b/addons/sale_product_configurator/wizard/sale_product_configurator.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class SaleProductConfigurator(models.TransientModel): + _name = 'sale.product.configurator' + _description = 'Sale Product Configurator' + + product_template_id = fields.Many2one( + 'product.template', string="Product", + required=True, domain=[('sale_ok', '=', True), '|', ('attribute_line_ids.value_ids', '!=', False), ('optional_product_ids', '!=', False)]) + pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True) diff --git a/addons/sale_product_configurator/wizard/sale_product_configurator_views.xml b/addons/sale_product_configurator/wizard/sale_product_configurator_views.xml new file mode 100644 index 00000000000..a3d3eddfee9 --- /dev/null +++ b/addons/sale_product_configurator/wizard/sale_product_configurator_views.xml @@ -0,0 +1,18 @@ + + + + sale.product.configurator.view.form + sale.product.configurator + +

+ + + +
+
+
+ + + diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 6a2daeee7da..9890566f03b 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -25,6 +25,17 @@
+
+
+ +
+
+
+
-- cgit v1.2.3