From 7f4013fc30b937f367b384b41342aec29be97d3b Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 27 Sep 2022 17:15:44 +0000 Subject: [PATCH] [REF] sale_product_configurator: convert configurator to OWL Task-2918791 X-original-commit: ab3e2c9b129e2cf74e9341b49bc0d15fb971ea33 Part-of: odoo/odoo#101352 --- .../sale_product_configurator/__manifest__.py | 5 +- .../controllers/main.py | 15 +- .../src/js/product_configurator_widget.js | 465 ------------------ .../static/src/js/sale_product_field.js | 260 ++++++++++ .../static/tests/product_configurator.test.js | 271 ---------- 5 files changed, 270 insertions(+), 746 deletions(-) delete mode 100644 addons/sale_product_configurator/static/src/js/product_configurator_widget.js create mode 100644 addons/sale_product_configurator/static/src/js/sale_product_field.js delete mode 100644 addons/sale_product_configurator/static/tests/product_configurator.test.js diff --git a/addons/sale_product_configurator/__manifest__.py b/addons/sale_product_configurator/__manifest__.py index 750545113a8..768b31b71d0 100644 --- a/addons/sale_product_configurator/__manifest__.py +++ b/addons/sale_product_configurator/__manifest__.py @@ -25,11 +25,8 @@ It also enables the "optional products" feature. 'assets': { 'web.assets_backend': [ 'sale/static/src/js/variant_mixin.js', - 'sale_product_configurator/static/src/js/product_configurator_widget.js', 'sale_product_configurator/static/src/js/product_configurator_modal.js', - ], - 'web.qunit_suite_tests': [ - 'sale_product_configurator/static/tests/product_configurator.test.js', + 'sale_product_configurator/static/src/js/sale_product_field.js', ], }, 'auto_install': True, diff --git a/addons/sale_product_configurator/controllers/main.py b/addons/sale_product_configurator/controllers/main.py index 847d4adfbd2..f56e0063cf9 100644 --- a/addons/sale_product_configurator/controllers/main.py +++ b/addons/sale_product_configurator/controllers/main.py @@ -21,12 +21,15 @@ class ProductConfiguratorController(http.Controller): if pricelist: product_template = product_template.with_context(pricelist=pricelist.id, partner=request.env.user.partner_id) - return request.env['ir.ui.view']._render_template("sale_product_configurator.configure", { - 'product': product_template, - 'pricelist': pricelist, - 'add_qty': add_qty, - 'product_combination': product_combination - }) + return request.env['ir.ui.view']._render_template( + "sale_product_configurator.configure", + { + 'product': product_template, + 'pricelist': pricelist, + 'add_qty': add_qty, + 'product_combination': product_combination + }, + ) @http.route(['/sale_product_configurator/show_advanced_configurator'], type='json', auth="user", methods=['POST']) def show_advanced_configurator(self, product_id, variant_values, pricelist_id, **kw): diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_widget.js b/addons/sale_product_configurator/static/src/js/product_configurator_widget.js deleted file mode 100644 index 0586722c4fd..00000000000 --- a/addons/sale_product_configurator/static/src/js/product_configurator_widget.js +++ /dev/null @@ -1,465 +0,0 @@ -/** @odoo-module */ -import ProductConfiguratorWidget from "sale.product_configurator"; -import { OptionalProductsModal } from "@sale_product_configurator/js/product_configurator_modal"; -import { - selectOrCreateProduct, - getSelectedVariantValues, - getNoVariantAttributeValues, -} from "sale.VariantMixin"; -import { _t } from "web.core"; - -/** - * Extension of the ProductConfiguratorWidget to support product configuration. - * It opens when a configurable product_template is set. - * (multiple variants, or custom attributes) - * - * The product customization information includes : - * - is_configurable_product - * - product_template_attribute_value_ids - * - */ -ProductConfiguratorWidget.include({ - /** - * Override of sale.product_configurator Hook - * - * @override - */ - _isConfigurableProduct: function () { - return this.recordData.is_configurable_product || this._super.apply(this, arguments); - }, - - /** - * Set restoreProductTemplateId for further backtrack. - * Saves the optional products in the widget for future application - * post-line configuration. - * - * {OdooEvent ev} - * {Array} ev.data.optionalProducts the various selected optional products - * with their configuration - * - * @override - * @private - */ - reset: function (record, ev) { - if (ev && ev.target === this) { - this.restoreProductTemplateId = this.recordData.product_template_id; - this.optionalProducts = (ev.data && ev.data.optionalProducts) || this.optionalProducts; - } - - this._super.apply(this, arguments); - }, - - /** - * This method is overridden to check if the product_template_id - * needs configuration or not: - * - * - The product_template has only one "product.product" and is not dynamic - * -> Set the product_id on the SO line - * -> If the product has optional products, open the configurator in 'options' mode - * - * - The product_template is configurable - * -> Open the product configurator wizard and initialize it with - * the provided product_template_id and its current attribute values - * - * @override - * @private - */ - _onTemplateChange: function (productTemplateId, dataPointId) { - var self = this; - var ctx = {}; - if (this.record && this.recordParams) { - ctx = this.record.getContext(this.recordParams); - } - - return this._rpc({ - model: "product.template", - method: "get_single_product_variant", - args: [productTemplateId], - context: ctx, - }).then(function (result) { - if (result.product_id && !result.has_optional_products) { - self.trigger_up("field_changed", { - dataPointID: dataPointId, - changes: { - product_id: { id: result.product_id }, - product_custom_attribute_value_ids: { - operation: "DELETE_ALL", - }, - }, - }); - } else { - return self._openConfigurator(result, productTemplateId, dataPointId); - } - // always returns true for the moment because no other configurator exists. - }); - }, - - /** - * When line is configured, apply the options defined earlier. - * @override - * @private - */ - _onLineConfigured: function () { - var self = this; - this._super.apply(this, arguments); - var parentList = self.getParent(); - var unselectRow = (parentList.unselectRow || function () {}).bind(parentList); // form view on mobile - if (self.optionalProducts && self.optionalProducts.length !== 0) { - self.trigger_up("add_record", { - context: self._productsToRecords(self.optionalProducts), - forceEditable: "bottom", - allowWarning: true, - onSuccess: function () { - // Leave edit mode of one2many list. - unselectRow(); - }, - }); - } else if (!self._isConfigurableLine() && self._isConfigurableProduct()) { - // Leave edit mode of current line if line was configured - // only through the product configurator. - unselectRow(); - } - }, - - _openConfigurator: function (result, productTemplateId, dataPointId) { - if (!result.mode || result.mode === "configurator") { - this._openProductConfigurator( - { - configuratorMode: result && result.has_optional_products ? "options" : "add", - pricelist_id: this._getPricelistId(), - product_template_id: productTemplateId, - }, - dataPointId - ); - return Promise.resolve(true); - } - return Promise.resolve(false); - }, - - /** - * Opens the product configurator to allow configuring the product template - * and its various options. - * - * The configuratorMode param controls how to open the configurator. - * - The "add" mode will allow configuring the product template & options. - * - The "edit" mode will only allow editing the product template's configuration. - * - The "options" mode is a special case where the product configurator is used as a bridge - * between the SO line and the optional products modal. It will hide its window and handle - * the communication between those two. - * - * When the configuration is canceled (i.e when the product configurator is closed using the - * "CANCEL" button or the cross on the top right corner of the window), - * the product_template is reset to its previous value if any. - * - * @param {Object} data various "default_" values - * {string} data.configuratorMode 'add' or 'edit' or 'options'. - * @param {string} dataPointId - * - * @private - */ - async _openProductConfigurator(data, dataPointId) { - this.optionalProducts = undefined; - const optionalProductsModal = await this._openModal(data); - // no optional products found for this product, only add the root product - optionalProductsModal.on("options_empty", null, () => - this._addProducts({ mainProduct: this.rootProduct }, dataPointId) - ); - - let confirmed = false; - optionalProductsModal.on("confirm", null, async () => { - confirmed = true; - const [ - mainProduct, - ...options - ] = await optionalProductsModal.getAndCreateSelectedProducts(); - this._addProducts({ mainProduct, options }, dataPointId); - }); - optionalProductsModal.on("closed", null, () => { - if (confirmed) { - return; - } - if (this.restoreProductTemplateId) { - // if configurator opened in edit mode. - this.trigger_up("field_changed", { - dataPointID: dataPointId, - preventProductIdCheck: true, - changes: { - product_template_id: this.restoreProductTemplateId.data, - }, - }); - } else { - // if configurator opened to create line: - // destroy line if configurator closed during configuration process. - this.trigger_up("field_changed", { - dataPointID: dataPointId, - changes: { - product_template_id: false, - product_id: false, - }, - }); - } - }); - }, - - async _openModal(data) { - const { - product_template_id, - product_template_attribute_value_ids, - product_no_variant_attribute_value_ids, - pricelist_id, - product_custom_attribute_value_ids, - quantity, - configuratorMode, - } = data; - // FIXME replace this with an RPC instead of rendering a template and querying it for data - const $modal = $( - await this._rpc({ - route: "/sale_product_configurator/configure", - params: { - product_template_id, - pricelist_id, - add_qty: quantity, - product_template_attribute_value_ids, - product_no_variant_attribute_value_ids, - context: this.getSession().user_context, - }, - }) - ); - const productSelector = `input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked`; - const productTemplateId = $modal.find(".product_template_id").val(); - const productId = await selectOrCreateProduct.call( - this, - $modal, - parseInt($modal.find(productSelector).first().val(), 10), - productTemplateId, - false - ); - $modal.find(productSelector).val(productId); - const variantValues = getSelectedVariantValues($modal); - const noVariantAttributeValues = getNoVariantAttributeValues($modal); - this.rootProduct = { - product_id: productId, - product_template_id: parseInt(productTemplateId), - quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1), - variant_values: variantValues, - product_custom_attribute_values: product_custom_attribute_value_ids, - no_variant_attribute_values: noVariantAttributeValues, - }; - return new OptionalProductsModal(null, { - rootProduct: this.rootProduct, - pricelistId: pricelist_id, - okButtonText: _t("Confirm"), - cancelButtonText: _t("Back"), - title: _t("Configure"), - context: - (this.record && this.recordParams && this.record.getContext(this.recordParams)) || - {}, - previousModalHeight: this.$el.closest(".modal-content").height(), - mode: configuratorMode, - }).open(); - }, - - /** - * Opens the product configurator in "edit" mode. - * (see '_openProductConfigurator' for more info on the "edit" mode). - * The requires to retrieve all the needed data from the SO line - * that are kept in the "recordData" object. - * - * @private - */ - _onEditProductConfiguration: function () { - if (!this.recordData.is_configurable_product) { - // if line should be edited by another configurator - // or simply inline. - this._super.apply(this, arguments); - return; - } - this.restoreProductTemplateId = this.recordData.product_template_id; - // If line has been set up through the product_configurator: - this._openProductConfigurator( - { - configuratorMode: "edit", - product_template_id: this.recordData.product_template_id.data.id, - pricelist_id: this._getPricelistId(), - product_template_attribute_value_ids: this._toIdArray( - this.recordData.product_template_attribute_value_ids - ), - product_no_variant_attribute_value_ids: this._toIdArray( - this.recordData.product_no_variant_attribute_value_ids - ), - product_custom_attribute_value_ids: this.recordData.product_custom_attribute_value_ids.data.map( - (d) => d.data - ), - quantity: this.recordData.product_uom_qty, - }, - this.dataPointID - ); - }, - - _toIdArray(recordData) { - return recordData ? [...recordData.res_ids] : null; - }, - - /** - * This will first modify the SO line to update all the information coming from - * the product configurator using the 'field_changed' event. - * - * onSuccess from that first method, it will add the optional products to the SO - * using the 'add_record' event. - * - * Doing both at the same time could lead to unordered product_template/options. - * - * @param {Object} products the products to add to the SO line. - * {Object} products.mainProduct the product_template configured - * with various attribute/custom values - * {Array} products.options the various selected optional products - * with their configuration - * @param {string} dataPointId - * - * @private - */ - _addProducts: function (result, dataPointId) { - this.trigger_up("field_changed", { - dataPointID: dataPointId, - preventProductIdCheck: true, - optionalProducts: result.options, - changes: this._getMainProductChanges(result.mainProduct), - }); - }, - - /** - * This will convert the result of the product configurator into - * "changes" that are understood by the basic_model.js - * - * For the product_custom_attribute_value_ids, we need to do a DELETE_ALL - * command to clean the currently selected values and then a CREATE for every - * custom value specified in the configurator. - * - * For the product_no_variant_attribute_value_ids, we also need to do a DELETE_ALL - * command to clean the currently selected values and issue a single ADD_M2M containing - * all the ids of the product_attribute_values. - * - * @param {Object} mainProduct - * - * @private - */ - _getMainProductChanges: function (mainProduct) { - var result = { - product_id: { id: mainProduct.product_id }, - product_template_id: { id: mainProduct.product_template_id }, - product_uom_qty: mainProduct.quantity, - }; - - var customAttributeValues = mainProduct.product_custom_attribute_values; - var customValuesCommands = [{ operation: "DELETE_ALL" }]; - if (customAttributeValues && customAttributeValues.length !== 0) { - _.each(customAttributeValues, function (customValue) { - // FIXME awa: This could be optimized by adding a "disableDefaultGet" to avoid - // having multiple default_get calls that are useless since we already - // have all the default values locally. - // However, this would mean a lot of changes in basic_model.js to handle - // those "default_" values and set them on the various fields (text,o2m,m2m,...). - // -> This is not considered as worth it right now. - customValuesCommands.push({ - operation: "CREATE", - context: [ - { - default_custom_product_template_attribute_value_id: - customValue.custom_product_template_attribute_value_id, - default_custom_value: customValue.custom_value, - }, - ], - }); - }); - } - - result["product_custom_attribute_value_ids"] = { - operation: "MULTI", - commands: customValuesCommands, - }; - - var noVariantAttributeValues = mainProduct.no_variant_attribute_values; - var noVariantCommands = [{ operation: "DELETE_ALL" }]; - if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) { - var resIds = _.map(noVariantAttributeValues, function (noVariantValue) { - return { id: parseInt(noVariantValue.value) }; - }); - - noVariantCommands.push({ - operation: "ADD_M2M", - ids: resIds, - }); - } - - result["product_no_variant_attribute_value_ids"] = { - operation: "MULTI", - commands: noVariantCommands, - }; - - return result; - }, - - /** - * Returns the pricelist_id set on the sale_order form - * - * @private - * @returns {integer} pricelist_id's id - */ - _getPricelistId: function () { - return this.record.evalContext.parent.pricelist_id; - }, - - /** - * Will map the products to appropriate record objects that are - * ready for the default_get. - * - * @param {Array} products The products to transform into records - * - * @private - */ - _productsToRecords: function (products) { - var records = []; - _.each(products, function (product) { - var record = { - default_product_id: product.product_id, - default_product_template_id: product.product_template_id, - default_product_uom_qty: product.quantity, - }; - - if (product.no_variant_attribute_values) { - var defaultProductNoVariantAttributeValues = []; - _.each(product.no_variant_attribute_values, function (attributeValue) { - defaultProductNoVariantAttributeValues.push([ - 4, - parseInt(attributeValue.value), - ]); - }); - record[ - "default_product_no_variant_attribute_value_ids" - ] = defaultProductNoVariantAttributeValues; - } - - if (product.product_custom_attribute_values) { - var defaultCustomAttributeValues = []; - _.each(product.product_custom_attribute_values, function (attributeValue) { - defaultCustomAttributeValues.push([ - 0, - 0, - { - custom_product_template_attribute_value_id: - attributeValue.custom_product_template_attribute_value_id, - custom_value: attributeValue.custom_value, - }, - ]); - }); - record["default_product_custom_attribute_value_ids"] = defaultCustomAttributeValues; - } - - records.push(record); - }); - - return records; - }, -}); - -export { ProductConfiguratorWidget }; diff --git a/addons/sale_product_configurator/static/src/js/sale_product_field.js b/addons/sale_product_configurator/static/src/js/sale_product_field.js new file mode 100644 index 00000000000..9a6e1906659 --- /dev/null +++ b/addons/sale_product_configurator/static/src/js/sale_product_field.js @@ -0,0 +1,260 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { useService } from "@web/core/utils/hooks"; +import { SaleOrderLineProductField } from '@sale/js/sale_product_field'; +import { OptionalProductsModal } from "@sale_product_configurator/js/product_configurator_modal"; +import { + selectOrCreateProduct, + getSelectedVariantValues, + getNoVariantAttributeValues, +} from "sale.VariantMixin"; + + +patch(SaleOrderLineProductField.prototype, 'sale_product_configurator', { + + setup() { + this._super(...arguments); + + this.rpc = useService("rpc"); + this.ui = useService("ui"); + }, + + async _onProductTemplateUpdate() { + this._super(...arguments); + const result = await this.orm.call( + 'product.template', + 'get_single_product_variant', + [this.props.record.data.product_template_id[0]], + ); + if(result && result.product_id) { + if (this.props.record.data.product_id != result.product_id.id) { + await this.props.record.update({ + product_id: [result.product_id, 'whatever'], + }); + if (result.has_optional_products) { + this._openProductConfigurator('options'); + } else { + this._onProductUpdate(); + } + } + } else { + if (!result.mode || result.mode === 'configurator') { + this._openProductConfigurator('add'); + } else { + // only triggered when sale_product_matrix is installed. + this._openGridConfigurator(result.mode); + } + } + }, + + _editProductConfiguration() { + this._super(...arguments); + if (this.props.record.data.is_configurable_product) { + this._openProductConfigurator('edit'); + } + }, + + get isConfigurableTemplate() { + return this._super(...arguments) || this.props.record.data.is_configurable_product; + }, + + async _openProductConfigurator(mode) { + const saleOrderRecord = this.props.record.model.root; + const pricelistId = saleOrderRecord.data.pricelist_id ? saleOrderRecord.data.pricelist_id[0] : false; + const productTemplateId = this.props.record.data.product_template_id[0]; + const $modal = $( + await this.rpc( + "/sale_product_configurator/configure", + { + product_template_id: productTemplateId, + quantity: this.props.record.data.product_uom_qty || 1, + pricelist_id: pricelistId, + product_template_attribute_value_ids: this.props.record.data.product_template_attribute_value_ids.records.map( + record => record.data.id + ), + product_no_variant_attribute_value_ids: this.props.record.data.product_no_variant_attribute_value_ids.records.map( + record => record.data.id + ), + context: this.props.record.context, + }, + ) + ); + const productSelector = `input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked`; + // TODO VFE drop this selectOrCreate and make it so that + // get_single_product_variant returns first variant as well. + // and use specified product on edition mode. + const productId = await selectOrCreateProduct.call( + this, + $modal, + parseInt($modal.find(productSelector).first().val(), 10), + productTemplateId, + false + ); + $modal.find(productSelector).val(productId); + const variantValues = getSelectedVariantValues($modal); + const noVariantAttributeValues = getNoVariantAttributeValues($modal); + const customAttributeValues = this.props.record.data.product_custom_attribute_value_ids.records.map( + record => { + // NOTE: this dumb formatting is necessary to avoid + // modifying the shared code between frontend & backend for now. + return { + custom_value: record.data.custom_value, + custom_product_template_attribute_value_id: { + res_id: record.data.custom_product_template_attribute_value_id[0], + }, + }; + } + ); + this.rootProduct = { + product_id: productId, + product_template_id: productTemplateId, + quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1), + variant_values: variantValues, + product_custom_attribute_values: customAttributeValues, + no_variant_attribute_values: noVariantAttributeValues, + }; + const optionalProductsModal = new OptionalProductsModal(null, { + rootProduct: this.rootProduct, + pricelistId: pricelistId, + okButtonText: this.env._t("Confirm"), + cancelButtonText: this.env._t("Back"), + title: this.env._t("Configure"), + context: this.props.record.context, + mode: mode, + }); + let modalEl; + optionalProductsModal.opened(() => { + modalEl = optionalProductsModal.el; + this.ui.activateElement(modalEl); + }); + optionalProductsModal.on("closed", null, async () => { + // Wait for the event that caused the close to bubble + await new Promise(resolve => setTimeout(resolve, 0)); + this.ui.deactivateElement(modalEl); + }); + optionalProductsModal.open(); + + let confirmed = false; + optionalProductsModal.on("confirm", null, async () => { + confirmed = true; + const [ + mainProduct, + ...optionalProducts + ] = await optionalProductsModal.getAndCreateSelectedProducts(); + + await this.props.record.update(this._convertConfiguratorDataToUpdateData(mainProduct)); + this._onProductUpdate(); + const optionalProductLinesCreationContext = this._convertConfiguratorDataToLinesCreationContext(optionalProducts); + for (let optionalProductLineCreationContext of optionalProductLinesCreationContext) { + const line = await saleOrderRecord.data.order_line.addNew({ + position: 'bottom', + context: optionalProductLineCreationContext, + mode: 'readonly', // whatever but not edit ! + }); + // FIXME: update sets the field dirty otherwise on the next edit and click out it gets deleted + line.update({ sequence: line.data.sequence }); + } + saleOrderRecord.data.order_line.unselectRecord(); + }); + optionalProductsModal.on("closed", null, () => { + if (confirmed) { + return; + } + if (mode != 'edit') { + this.props.record.update({ + product_template_id: false, + product_id: false, + product_uom_qty: 1.0, + // TODO reset custom/novariant values (and remove onchange logic?) + }); + } + }); + }, + + _convertConfiguratorDataToUpdateData(mainProduct) { + let result = { + // TODO find a way to get the real product name + // bc 'whatever' is really displayed when showing the 'Product Variant' column + product_id: [mainProduct.product_id, 'whatever'], + product_uom_qty: mainProduct.quantity, + }; + var customAttributeValues = mainProduct.product_custom_attribute_values; + var customValuesCommands = [{ operation: "DELETE_ALL" }]; + if (customAttributeValues && customAttributeValues.length !== 0) { + _.each(customAttributeValues, function (customValue) { + customValuesCommands.push({ + operation: "CREATE", + context: [ + { + default_custom_product_template_attribute_value_id: + customValue.custom_product_template_attribute_value_id, + default_custom_value: customValue.custom_value, + }, + ], + }); + }); + } + + result.product_custom_attribute_value_ids = { + operation: "MULTI", + commands: customValuesCommands, + }; + + var noVariantAttributeValues = mainProduct.no_variant_attribute_values; + var noVariantCommands = [{ operation: "DELETE_ALL" }]; + if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) { + var resIds = _.map(noVariantAttributeValues, function (noVariantValue) { + return { id: parseInt(noVariantValue.value) }; + }); + + noVariantCommands.push({ + operation: "ADD_M2M", + ids: resIds, + }); + } + + result.product_no_variant_attribute_value_ids = { + operation: "MULTI", + commands: noVariantCommands, + }; + + return result; + }, + + /** + * Will map the optional producs data to sale.order.line + * creation contexts. + * + * @param {Array} optionalProductsData The optional products data given by the configurator + * + * @private + */ + _convertConfiguratorDataToLinesCreationContext: function (optionalProductsData) { + return optionalProductsData.map(productData => { + return { + default_product_id: productData.product_id, + default_product_template_id: productData.product_template_id, + default_product_uom_qty: productData.quantity, + default_product_no_variant_attribute_value_ids: productData.no_variant_attribute_values.map( + noVariantAttributeData => { + return [4, parseInt(noVariantAttributeData.value)]; + } + ), + default_product_custom_attribute_value_ids: productData.product_custom_attribute_values.map( + customAttributeData => { + return [ + 0, + 0, + { + custom_product_template_attribute_value_id: + customAttributeData.custom_product_template_attribute_value_id, + custom_value: customAttributeData.custom_value, + }, + ]; + } + ) + }; + }); + }, +}); diff --git a/addons/sale_product_configurator/static/tests/product_configurator.test.js b/addons/sale_product_configurator/static/tests/product_configurator.test.js deleted file mode 100644 index 3db2b281df3..00000000000 --- a/addons/sale_product_configurator/static/tests/product_configurator.test.js +++ /dev/null @@ -1,271 +0,0 @@ -/** @odoo-module */ -import FormView from 'web.FormView'; -import testUtils from 'web.test_utils'; -import { patchWithCleanup } from "@web/../tests/helpers/utils"; -import { ProductConfiguratorWidget } from "@sale_product_configurator/js/product_configurator_widget"; -const createView = testUtils.createView; - -const 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_template_id: { - string: 'product template', - type: 'many2one', - relation: 'product_template' - }, - product_id: { - string: 'product', - type: 'many2one', - relation: 'product' - }, - product_custom_attribute_value_ids: { - string: 'product_custom_attribute_values', - type: 'one2many', - relation: 'product_custom_attribute_value' - }, - product_uom_qty: {type: 'integer'}, - sequence: {type: 'integer'}, - } - }, - product_custom_attribute_value: { - fields: { - id: {type: 'integer'}, - sale_order_line_id: { - string: 'sale order line', - type: 'many2one', - relation: 'sale_order_line' - } - } - }, - sale_product_configurator: { - fields: { - product_template_id: { - string: 'product', - type: 'many2one', - relation: 'product_template' - }, - product_template_attribute_value_ids: { - type: 'many2many', - relation: 'product_template_attribute_value' - }, - product_no_variant_attribute_value_ids: { - type: 'many2many', - relation: 'product_template_attribute_value' - }, - product_custom_attribute_value_ids: { - type: 'many2many', - relation: 'product_attribute_custom_value' - } - }, - records: [{ - product_template_id: 42 - }] - }, - product_template_attribute_value: { - fields: { - id: {type: 'integer'} - } - }, - product_attribute_custom_value: { - fields: { - id: {type: 'integer'} - } - }, - pricelist: { - fields: { - id: {type: 'integer'} - } - } - }; - } -}, function () { - QUnit.test('Select a non configurable product template and verify that the product_id is correctly set', async function (assert) { - assert.expect(2); - - const form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: getArch(), - mockRPC: function (route, params) { - if (params.method === 'get_single_product_variant') { - assert.ok(true); - return Promise.resolve({product_id: 2}); - } - // FIXME awa: this shouldn't be here since the read is done in 'event_sale' - // But at the moment there is no easy way to solve such cross module 'include' issues - if (params.method === 'read') { - return Promise.resolve(false); - } - return this._super.apply(this, arguments); - }, - intercepts: { - do_action: function (ev) { - if (ev.data.action === 'sale_product_configurator.sale_product_configurator_action') { - assert.ok(false, "Should not execute the configure action"); - } - }, - } - }); - - await testUtils.dom.click(form.$("a:contains('Add a product')")); - await testUtils.fields.many2one.searchAndClickItem("product_template_id", {item: 'Customizable Desk'}); - // check that product_id is correctly set to 2 - assert.strictEqual(form.renderer.state.data.sale_order_line.data[0].data.product_id.data.id, 2); - form.destroy(); - }); - - QUnit.test('Select a configurable product template and verify that the product configurator is opened', async function (assert) { - patchWithCleanup(ProductConfiguratorWidget.prototype, { - _openProductConfigurator() { - assert.step("open configurator"); - } - }); - const form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: getArch(), - mockRPC: function (route, params) { - if (params.method === 'get_single_product_variant') { - assert.ok(true); - return Promise.resolve(false); - } - return this._super.apply(this, arguments); - }, - }); - - await testUtils.dom.click(form.$("a:contains('Add a product')")); - assert.verifySteps([]); - await testUtils.fields.many2one.searchAndClickItem("product_template_id", {item: 'Customizable Desk'}); - assert.verifySteps(["open configurator"]); - 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); - - const form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: getArch() - }); - - let 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('drag and drop rows containing product_configurator many2one', async function (assert) { - assert.expect(4); - - this.data.sale_order.records = [ - { id: 1, sale_order_line: [1, 2] } - ]; - this.data.sale_order_line.records = [ - { id: 1, sequence: 5, product_id: 1 }, - { id: 2, sequence: 15, product_id: 2 }, - ]; - - const form = await createView({ - View: FormView, - model: 'sale_order', - data: this.data, - arch: ` -
- - `, - archs: { - 'sale_order_line,false,list': ` - - - - `, - }, - res_id: 1, - viewOptions: { - mode: 'edit', - }, - }); - - assert.containsN(form, '.o_data_row', 2); - assert.strictEqual(form.$('.o_data_row').text(), 'Customizable Desk (1)Customizable Desk (2)'); - assert.containsN(form, '.o_data_row .o_row_handle', 2); - - // move first row below second - const $firstHandle = form.$('.o_data_row:nth(0) .o_row_handle'); - const $secondHandle = form.$('.o_data_row:nth(1) .o_row_handle'); - await testUtils.dom.dragAndDrop($firstHandle, $secondHandle, { position: 'bottom' }); - - assert.strictEqual(form.$('.o_data_row').text(), 'Customizable Desk (2)Customizable Desk (1)'); - - form.destroy(); - }); -});