[REF] sale_product_configurator: convert configurator to OWL

Task-2918791

X-original-commit: ab3e2c9b129e2cf74e9341b49bc0d15fb971ea33
Part-of: odoo/odoo#101352
This commit is contained in:
Victor Feyens
2022-09-28 11:14:35 +02:00
parent ab7846be8e
commit 7f4013fc30
5 changed files with 270 additions and 746 deletions
@@ -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,
@@ -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):
@@ -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 };
@@ -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,
},
];
}
)
};
});
},
});
@@ -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 '<form>' +
'<sheet>' +
'<field name="pricelist_id" widget="selection" />' +
'<field name="sale_order_line" widget="section_and_note_one2many">' +
'<tree editable="top"><control>' +
'<create string="Add a product"/>' +
'<create string="Add a section" context="{\'default_display_type\': \'line_section\'}"/>' +
'<create string="Add a note" context="{\'default_display_type\': \'line_note\'}"/>' +
'</control>' +
'<field name="product_id" invisible="1"/>' +
'<field name="product_template_id" widget="product_configurator"/>' +
'<field name="product_uom_qty"/>' +
'<field name="product_custom_attribute_value_ids" invisible="1"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>';
};
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: `
<form>
<field name="sale_order_line"/>
</form>`,
archs: {
'sale_order_line,false,list': `
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="product_id" widget="product_configurator"/>
</tree>`,
},
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();
});
});