Initial commit of sale_line_reconfigure for 12.0

This commit is contained in:
Jared Kipe
2019-02-13 19:02:46 -08:00
parent 319a55431d
commit 432944cfaf
13 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
odoo.define('sale_line_reconfigure.ProductConfiguratorFormController', function (require) {
"use strict";
/*
Product Configurator is very powerful, but not so easy to extend.
*/
var core = require('web.core');
var _t = core._t;
var FormController = require('web.FormController');
var OptionalProductsModal = require('sale.OptionalProductsModal');
var ProductConfiguratorFormController = FormController.extend({
custom_events: _.extend({}, FormController.prototype.custom_events, {
field_changed: '_onFieldChanged'
}),
className: 'o_product_configurator',
/**
* @override
*/
init: function (){
this._super.apply(this, arguments);
},
/**
* We need to override the default click behavior for our "Add" button
* because there is a possibility that this product has optional products.
* If so, we need to display an extra modal to choose the options.
*
* @override
*/
_onButtonClicked: function (event) {
if (event.stopPropagation){
event.stopPropagation();
}
var attrs = event.data.attrs;
if (attrs.special === 'cancel') {
this._super.apply(this, arguments);
} else {
if (!this.$el
.parents('.modal')
.find('.o_sale_product_configurator_add')
.hasClass('disabled')){
this._handleAdd(this.$el);
}
}
},
/**
* This is overridden to allow catching the "select" event on our product template select field.
* This will not work anymore if more fields are added to the form.
* TODO awa: Find a better way to catch that event.
*
* @override
*/
_onFieldChanged: function (event) {
var self = this;
this.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled');
this._rpc({
route: '/product_configurator/configure',
params: {
product_id: event.data.changes.product_template_id.id,
pricelist_id: this.renderer.pricelistId,
sale_line_id: this.renderer.saleLineId,
}
}).then(function (configurator) {
self.renderer.renderConfigurator(configurator);
});
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* When the user adds a product that has optional products, we need to display
* a window to allow the user to choose these extra options.
*
* 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);
var productReady = this.renderer.selectOrCreateProduct(
$modal,
productId,
$modal.find('.product_template_id').val(),
false
);
productReady.done(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')
}).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;
});

View File

@@ -0,0 +1,55 @@
odoo.define('sale_line_reconfigure.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
this.saleLineId = this.state.context.default_sale_line_id || 0;
// End Override
},
/**
* @override
*/
start: function () {
this._super.apply(this, arguments);
this.$el.append($('<div>', {class: 'configurator_container'}));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Renders the product configurator within the form
*
* Will also:
* - add events handling for variant changes
* - trigger variant change to compute the price and other
* variant specific changes
*
* @param {string} configuratorHtml the evaluated template of
* the product configurator
*/
renderConfigurator: function (configuratorHtml) {
var $configuratorContainer = this.$('.configurator_container');
$configuratorContainer.empty();
var $configuratorHtml = $(configuratorHtml);
$configuratorHtml.appendTo($configuratorContainer);
this.triggerVariantChange($configuratorContainer);
}
});
return ProductConfiguratorFormRenderer;
});

View File

@@ -0,0 +1,20 @@
odoo.define('sale_line_reconfigure.ProductConfiguratorFormView', function (require) {
"use strict";
var ProductConfiguratorFormController = require('sale_line_reconfigure.ProductConfiguratorFormController');
var ProductConfiguratorFormRenderer = require('sale_line_reconfigure.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;
});

View File

@@ -0,0 +1,227 @@
odoo.define('sale_line_reconfigure.section_and_note_backend', function (require) {
// The goal of this file is to contain JS hacks related to allowing
// section and note on sale order and invoice.
// [UPDATED] now also allows configuring products on sale order.
// This is a copy job from `account` to support re-configuring a selected sale order line.
"use strict";
var pyUtils = require('web.py_utils');
var core = require('web.core');
var _t = core._t;
var FieldChar = require('web.basic_fields').FieldChar;
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var fieldRegistry = require('web.field_registry');
var FieldText = require('web.basic_fields').FieldText;
var ListRenderer = require('web.ListRenderer');
var SectionAndNoteListRenderer = ListRenderer.extend({
/**
* We want section and note to take the whole line (except handle and trash)
* to look better and to hide the unnecessary fields.
*
* @override
*/
_renderBodyCell: function (record, node, index, options) {
var $cell = this._super.apply(this, arguments);
var isSection = record.data.display_type === 'line_section';
var isNote = record.data.display_type === 'line_note';
if (isSection || isNote) {
if (node.attrs.widget === "handle") {
return $cell;
} else if (node.attrs.name === "name") {
var nbrColumns = this._getNumberOfCols();
if (this.handleField) {
nbrColumns--;
}
if (this.addTrashIcon) {
nbrColumns--;
}
$cell.attr('colspan', nbrColumns);
} else {
return $cell.addClass('o_hidden');
}
}
return $cell;
},
/**
* We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS.
*
* @override
*/
_renderRow: function (record, index) {
var $row = this._super.apply(this, arguments);
if (record.data.display_type) {
$row.addClass('o_is_' + record.data.display_type);
}
return $row;
},
/**
* We want to add .o_section_and_note_list_view on the table to have stronger CSS.
*
* @override
* @private
*/
_renderView: function () {
var def = this._super();
this.$el.find('> table').addClass('o_section_and_note_list_view');
return def;
},
/**
* Add support for product configurator
*
* @override
* @private
*/
_onAddRecord: function (ev) {
// we don't want the browser to navigate to a the # url
ev.preventDefault();
// we don't want the click to cause other effects, such as unselecting
// the row that we are creating, because it counts as a click on a tr
ev.stopPropagation();
var lineId = null;
if (this.currentRow !== null && this.state.data[this.currentRow].res_id) {
lineId = this.state.data[this.currentRow].res_id;
}
// but we do want to unselect current row
var self = this;
this.unselectRow().then(function () {
var context = ev.currentTarget.dataset.context;
var pricelistId = self._getPricelistId();
if (context && pyUtils.py_eval(context).open_product_configurator){
self._rpc({
model: 'ir.model.data',
method: 'xmlid_to_res_id',
kwargs: {xmlid: 'sale.sale_product_configurator_view_form'},
}).then(function (res_id) {
self.do_action({
name: _t('Configure a product'),
type: 'ir.actions.act_window',
res_model: 'sale.product.configurator',
views: [[res_id, 'form']],
target: 'new',
context: {
'default_pricelist_id': pricelistId,
'default_sale_line_id': lineId,
}
}, {
on_close: function (products) {
if (products && products !== 'special'){
self.trigger_up('add_record', {
context: self._productsToRecords(products),
forceEditable: "bottom" ,
allowWarning: true,
onSuccess: function (){
self.unselectRow();
}
});
}
}
});
});
} else {
self.trigger_up('add_record', {context: context && [context]}); // TODO write a test, the deferred was not considered
}
});
},
/**
* Will try to get the pricelist_id value from the parent sale_order form
*
* @private
* @returns {integer} pricelist_id's id
*/
_getPricelistId: function () {
var saleOrderForm = this.getParent() && this.getParent().getParent();
var stateData = saleOrderForm && saleOrderForm.state && saleOrderForm.state.data;
var pricelist_id = stateData.pricelist_id && stateData.pricelist_id.data && stateData.pricelist_id.data.id;
return pricelist_id;
},
/**
* Will map the products to appropriate record objects that are
* ready for the default_get
*
* @private
* @param {Array} products The products to transform into records
*/
_productsToRecords: function (products) {
var records = [];
_.each(products, function (product){
var record = {
default_product_id: product.product_id,
default_product_uom_qty: product.quantity
};
if (product.no_variant_attribute_values) {
var default_product_no_variant_attribute_values = [];
_.each(product.no_variant_attribute_values, function (attribute_value) {
default_product_no_variant_attribute_values.push(
[4, parseInt(attribute_value.value)]
);
});
record['default_product_no_variant_attribute_value_ids']
= default_product_no_variant_attribute_values;
}
if (product.product_custom_attribute_values) {
var default_custom_attribute_values = [];
_.each(product.product_custom_attribute_values, function (attribute_value) {
default_custom_attribute_values.push(
[0, 0, {
attribute_value_id: attribute_value.attribute_value_id,
custom_value: attribute_value.custom_value
}]
);
});
record['default_product_custom_attribute_value_ids']
= default_custom_attribute_values;
}
records.push(record);
});
return records;
}
});
// We create a custom widget because this is the cleanest way to do it:
// to be sure this custom code will only impact selected fields having the widget
// and not applied to any other existing ListRenderer.
var SectionAndNoteFieldOne2Many = FieldOne2Many.extend({
/**
* We want to use our custom renderer for the list.
*
* @override
*/
_getRenderer: function () {
if (this.view.arch.tag === 'tree') {
return SectionAndNoteListRenderer;
}
return this._super.apply(this, arguments);
},
});
// This is a merge between a FieldText and a FieldChar.
// We want a FieldChar for section,
// and a FieldText for the rest (product and note).
var SectionAndNoteFieldText = function (parent, name, record, options) {
var isSection = record.data.display_type === 'line_section';
var Constructor = isSection ? FieldChar : FieldText;
return new Constructor(parent, name, record, options);
};
fieldRegistry.add('section_and_note_one2many', SectionAndNoteFieldOne2Many);
fieldRegistry.add('section_and_note_text', SectionAndNoteFieldText);
});