[REF] product_configurator
Purposes: * Remove bridge module event_sale_product_configurator * Ease developement of product configurators AND product-based sale_order_line configurators * Fix the tests for those extensions * Note that despite the widget being extensible, the tests from the other configurators can easily crash when other configurators are installed because of query counts or limited data available in tests environments. Technical clarifications: A main product_configurator widget base in odoo/addons/sale, set on the product_product and product_template on the sale_order_line inline list, providing: * 5 hooks: * _onTemplateChange for product_product configurators based on product templates * _onProductChange for sale_order_line configurators based on product_product Those two hooks are expected to return a boolean promise to know whether the line has been configured or not, avoiding the opening of multiple wizards. * _isConfigurableProduct * _isConfigurableLine If true, a pencil button is shown next to the product/product_template, calling _onEditProductConfiguration when clicked. * _onEditProductConfiguration Called when clicking on edit pencil button. Examples : Product configurator : odoo/addons/sale_product_configurator module --> product_configurator (true one, the one in sale doesn't do anything until extended) Sale Order Line configurator : odoo/addons/event_sale/ --> event_configurator, future rental configurator
This commit is contained in:
@@ -1,59 +1,142 @@
|
||||
odoo.define('event.configurator', function (require) {
|
||||
|
||||
var relationalFields = require('web.relational_fields');
|
||||
var FieldsRegistry = require('web.field_registry');
|
||||
var EventConfiguratorWidgetMixin = require('event_sale.EventConfiguratorWidgetMixin');
|
||||
odoo.define('event_sale.product_configurator', function (require) {
|
||||
var ProductConfiguratorWidget = require('sale.product_configurator');
|
||||
|
||||
/**
|
||||
* The event configurator widget is a simple FieldMany2One that adds the capability
|
||||
* to configure a product_id with event information using the event configurator wizard.
|
||||
* Extension of the ProductConfiguratorWidget to support event product configuration.
|
||||
* It opens when an event product_product is set.
|
||||
*
|
||||
* The event information include:
|
||||
* - event_id
|
||||
* - event_ticket_id
|
||||
*
|
||||
* !!! It should only be used on a product_id field !!!
|
||||
*/
|
||||
var EventConfiguratorWidget = relationalFields.FieldMany2One.extend(EventConfiguratorWidgetMixin, {
|
||||
events: _.extend({},
|
||||
relationalFields.FieldMany2One.prototype.events,
|
||||
EventConfiguratorWidgetMixin.events
|
||||
),
|
||||
ProductConfiguratorWidget.include({
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_isConfigurableLine: function () {
|
||||
return this.recordData.event_ok || this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @see _addEventConfigurationEditButton for more info
|
||||
* @param {integer} productId
|
||||
* @param {String} dataPointID
|
||||
* @returns {Promise<Boolean>} stopPropagation true if a suitable configurator has been found.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
start: function () {
|
||||
_onProductChange: function (productId, dataPointId) {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function (stopPropagation) {
|
||||
if (stopPropagation) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return self._checkForEvent(productId, dataPointId);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method will check if the productId needs configuration or not:
|
||||
*
|
||||
* @param {integer} productId
|
||||
* @param {string} dataPointID
|
||||
* @returns {Promise<Boolean>} stopPropagation true if the product is an event ticket.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_checkForEvent: function (productId, dataPointId) {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._addEventConfigurationEditButton();
|
||||
return this._rpc({
|
||||
model: 'product.product',
|
||||
method: 'read',
|
||||
args: [productId, ['event_ok']],
|
||||
}).then(function (result) {
|
||||
if (result && result[0].event_ok) {
|
||||
self._openEventConfigurator({
|
||||
default_product_id: productId
|
||||
},
|
||||
dataPointId
|
||||
);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is overridden to check if the product_id needs configuration or not:
|
||||
* Opens the event configurator in 'edit' mode.
|
||||
*
|
||||
* @override
|
||||
* @param {OdooEvent} event
|
||||
* @private
|
||||
*/
|
||||
_onEditProductConfiguration: function () {
|
||||
if (this.recordData.event_ok) {
|
||||
var defaultValues = {
|
||||
default_product_id: this.recordData.product_id.data.id
|
||||
};
|
||||
|
||||
if (this.recordData.event_id) {
|
||||
defaultValues.default_event_id = this.recordData.event_id.data.id;
|
||||
}
|
||||
|
||||
if (this.recordData.event_ticket_id) {
|
||||
defaultValues.default_event_ticket_id = this.recordData.event_ticket_id.data.id;
|
||||
}
|
||||
|
||||
this._openEventConfigurator(defaultValues, this.dataPointID);
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the event configurator to allow configuring the SO line with events information.
|
||||
*
|
||||
* When the window is closed, configured values are used to trigger a 'field_changed'
|
||||
* event to modify the current SO line.
|
||||
*
|
||||
* If the window is closed without providing the required values 'event_id' and
|
||||
* 'event_ticket_id', the product_id field is cleaned.
|
||||
*
|
||||
* @param {Object} data various "default_" values
|
||||
* @param {string} dataPointId
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onFieldChanged: function (event) {
|
||||
_openEventConfigurator: function (data, dataPointId) {
|
||||
var self = this;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
if (!event.data.changes.product_id){
|
||||
return;
|
||||
}
|
||||
|
||||
var productId = event.data.changes.product_id.id;
|
||||
self._checkForEvent(productId, event.data.dataPointID);
|
||||
this.do_action('event_sale.event_configurator_action', {
|
||||
additional_context: data,
|
||||
on_close: function (result) {
|
||||
if (result && result !== 'special') {
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: dataPointId,
|
||||
changes: result.eventConfiguration,
|
||||
// VFE NOTE : Do we unselect the row after event config?
|
||||
// event_sale tour is made to work without unselectRow
|
||||
/*onSuccess: function (){
|
||||
self.getParent().unselectRow();
|
||||
}*/
|
||||
});
|
||||
} else {
|
||||
if (!self.recordData.event_id || !self.recordData.event_ticket_id) {
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: dataPointId,
|
||||
changes: {product_id: false},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
FieldsRegistry.add('event_configurator', EventConfiguratorWidget);
|
||||
|
||||
return EventConfiguratorWidget;
|
||||
return ProductConfiguratorWidget;
|
||||
|
||||
});
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
odoo.define('event_sale.EventConfiguratorWidgetMixin', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* This mixin intends to gather methods that will be used by the event configurator.
|
||||
* They're extracted here to be used by other widgets if necessary (namely the product_configurator_widget)
|
||||
*/
|
||||
var EventConfiguratorWidgetMixin = {
|
||||
events: {
|
||||
'click .o_event_sale_js_event_configurator_edit': '_onEditEventConfiguration'
|
||||
},
|
||||
/**
|
||||
* This method will check if the current product set on the SO line is a configurable event.
|
||||
* -> If so, we add a 'Edit Configuration' button next to the dropdown.
|
||||
*
|
||||
*/
|
||||
_addEventConfigurationEditButton: function () {
|
||||
var $inputDropdown = this.$('.o_input_dropdown');
|
||||
|
||||
if (this.recordData.event_ok &&
|
||||
$inputDropdown.length !== 0 &&
|
||||
this.$('.o_event_sale_js_event_configurator_edit').length === 0) {
|
||||
var $editConfigurationButton = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'fa fa-pencil btn btn-secondary o_event_sale_js_event_configurator_edit',
|
||||
tabindex: '-1',
|
||||
draggable: false,
|
||||
'aria-label': _t('Edit Configuration'),
|
||||
title: _t('Edit Configuration')
|
||||
});
|
||||
|
||||
$inputDropdown.after($editConfigurationButton);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method will check if the productId needs configuration or not:
|
||||
*
|
||||
* @param {integer} productId
|
||||
* @param {string} dataPointID
|
||||
*/
|
||||
_checkForEvent: function (productId, dataPointID) {
|
||||
var self = this;
|
||||
if (productId){
|
||||
this._rpc({
|
||||
model: 'product.product',
|
||||
method: 'read',
|
||||
args: [productId, ['event_ok']],
|
||||
}).then(function (result) {
|
||||
if (result && result[0].event_ok){
|
||||
self._openEventConfigurator({
|
||||
default_product_id: productId
|
||||
},
|
||||
dataPointID
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the event configurator in 'edit' mode.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onEditEventConfiguration: function () {
|
||||
var defaultValues = {
|
||||
default_product_id: this.recordData.product_id.data.id
|
||||
};
|
||||
|
||||
if (this.recordData.event_id) {
|
||||
defaultValues['default_event_id'] = this.recordData.event_id.data.id;
|
||||
}
|
||||
|
||||
if (this.recordData.event_ticket_id) {
|
||||
defaultValues['default_event_ticket_id'] = this.recordData.event_ticket_id.data.id;
|
||||
}
|
||||
|
||||
this._openEventConfigurator(defaultValues, this.dataPointID);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the event configurator to allow configuring the SO line with events information.
|
||||
*
|
||||
* When the window is closed, configured values are used to trigger a 'field_changed'
|
||||
* event to modify the current SO line.
|
||||
*
|
||||
* If the window is closed without providing the required values 'event_id' and
|
||||
* 'event_ticket_id', the product_id field is cleaned.
|
||||
*
|
||||
* @param {Object} data various "default_" values
|
||||
* @param {string} dataPointId
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_openEventConfigurator: function (data, dataPointId) {
|
||||
var self = this;
|
||||
this.do_action('event_sale.event_configurator_action', {
|
||||
additional_context: data,
|
||||
on_close: function (result) {
|
||||
if (result && result !== 'special'){
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: dataPointId,
|
||||
changes: result.eventConfiguration
|
||||
});
|
||||
} else {
|
||||
if (!self.recordData.event_id || !self.recordData.event_ticket_id) {
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: dataPointId,
|
||||
changes: {product_id: {operation: 'DELETE_ALL'}}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return EventConfiguratorWidgetMixin;
|
||||
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
.o_list_view {
|
||||
.o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) {
|
||||
.o_field_widget .o_event_sale_js_event_configurator_edit {
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ odoo.define('event.configurator.tests', function (require) {
|
||||
'<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" widget="event_configurator" />' +
|
||||
'<field name="product_id" widget="product_configurator" />' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
@@ -81,7 +81,7 @@ odoo.define('event.configurator.tests', function (require) {
|
||||
data: this.data,
|
||||
arch: getArch(),
|
||||
mockRPC: function (route, params) {
|
||||
if (params.method === 'read') {
|
||||
if (params.method === 'read' && params.args[1][0] === 'event_ok') {
|
||||
assert.ok(true);
|
||||
return Promise.resolve([{event_ok: false}]);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ odoo.define('event.configurator.tests', function (require) {
|
||||
data: this.data,
|
||||
arch: getArch(),
|
||||
mockRPC: function (route, params) {
|
||||
if (params.method === 'read') {
|
||||
if (params.method === 'read' && params.args[1][0] === 'event_ok') {
|
||||
assert.ok(true);
|
||||
return Promise.resolve([{event_ok: true}]);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ odoo.define('event.configurator.tests', function (require) {
|
||||
});
|
||||
|
||||
await testUtils.dom.click(form.$("a:contains('Add a product')"));
|
||||
await testUtils.fields.many2one.searchAndClickItem("product_id", {item: 'Customizable Event'})
|
||||
await testUtils.fields.many2one.searchAndClickItem("product_id", {item: 'Customizable Event'});
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,17 +18,10 @@ tour.register('event_configurator_tour', {
|
||||
}, {
|
||||
trigger: "a:contains('Add a product')"
|
||||
}, {
|
||||
trigger: 'div[name="product_id"] input',
|
||||
run: function (){
|
||||
var $input = $('div[name="product_id"] input');
|
||||
$input.click();
|
||||
$input.val('EVENT');
|
||||
// fake keydown to trigger search
|
||||
var keyDownEvent = jQuery.Event("keydown");
|
||||
keyDownEvent.which = 42;
|
||||
$input.trigger(keyDownEvent);
|
||||
},
|
||||
id: 'product_selection_step'
|
||||
trigger: 'div[name="product_id"] input, div[name="product_template_id"] input',
|
||||
run: function (actions) {
|
||||
actions.text('EVENT');
|
||||
}
|
||||
}, {
|
||||
trigger: 'ul.ui-autocomplete a:contains("EVENT")',
|
||||
run: 'click'
|
||||
@@ -52,7 +45,7 @@ tour.register('event_configurator_tour', {
|
||||
trigger: 'textarea[name="name"]',
|
||||
run: function () {
|
||||
var $textarea = $('textarea[name="name"]');
|
||||
if ($textarea.val().includes('Design Fair Los Angeles') && $textarea.val().includes('VIP')){
|
||||
if ($textarea.val().includes('Design Fair Los Angeles') && $textarea.val().includes('VIP')) {
|
||||
$textarea.addClass('tour_success');
|
||||
}
|
||||
}
|
||||
@@ -66,7 +59,7 @@ tour.register('event_configurator_tour', {
|
||||
trigger: 'td:contains("EVENT")',
|
||||
run: 'click'
|
||||
}, {
|
||||
trigger: '.o_event_sale_js_event_configurator_edit'
|
||||
trigger: '.o_edit_product_configuration'
|
||||
}, {
|
||||
trigger: 'div[name="event_ticket_id"] input',
|
||||
run: 'click'
|
||||
@@ -80,7 +73,7 @@ tour.register('event_configurator_tour', {
|
||||
trigger: 'textarea[name="name"]',
|
||||
run: function () {
|
||||
var $textarea = $('textarea[name="name"]');
|
||||
if ($textarea.val().includes('Standard')){
|
||||
if ($textarea.val().includes('Standard')) {
|
||||
$textarea.addClass('tour_success_2');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" name="event_sale assets backend">
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/event_sale/static/src/scss/event_configurator_widget.scss"/>
|
||||
</xpath>
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/event_sale/static/src/js/event_configurator_controller.js"></script>
|
||||
<script type="text/javascript" src="/event_sale/static/src/js/event_configurator_view.js"></script>
|
||||
<script type="text/javascript" src="/event_sale/static/src/js/event_configurator_widget_mixin.js"></script>
|
||||
<script type="text/javascript" src="/event_sale/static/src/js/event_configurator_widget.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<field name="event_ok" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']//tree//field[@name='product_id']" position="attributes">
|
||||
<attribute name="widget">event_configurator</attribute>
|
||||
<attribute name="options">{'no_open': True}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']//tree//field[@name='product_id']" position="after">
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': "Event Sale Product Configurator",
|
||||
'version': '1.0',
|
||||
'category': 'Hidden',
|
||||
'summary': "Bridge module between event_sale and sale_product_configurator",
|
||||
|
||||
'description': """
|
||||
Technical bridge module installed to make the event_configurator work on the product_template_id field
|
||||
added by the product configurator.
|
||||
""",
|
||||
|
||||
'depends': ['sale_product_configurator', 'event_sale'],
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
],
|
||||
'auto_install': True
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
odoo.define('event_sale.event_sale_product_configurator', function (require) {
|
||||
|
||||
var relationalFields = require('web.relational_fields');
|
||||
var ProductConfiguratorWidget = require('sale_product_configurator.product_configurator');
|
||||
var EventConfiguratorWidgetMixin = require('event_sale.EventConfiguratorWidgetMixin');
|
||||
|
||||
/**
|
||||
* The ProductConfiguratorWidget is overridden to add the features of the EventConfiguratorWidget.
|
||||
*/
|
||||
var productConfiguratorEvents = ProductConfiguratorWidget.prototype.events;
|
||||
ProductConfiguratorWidget.include(EventConfiguratorWidgetMixin);
|
||||
|
||||
ProductConfiguratorWidget.include({
|
||||
events: _.extend({},
|
||||
relationalFields.FieldMany2One.prototype.events,
|
||||
productConfiguratorEvents,
|
||||
EventConfiguratorWidgetMixin.events
|
||||
),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._addEventConfigurationEditButton();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {integer} productId
|
||||
* @param {string} dataPointID
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSimpleProductFound: function (productId, dataPointID) {
|
||||
this._checkForEvent(productId, dataPointID);
|
||||
}
|
||||
});
|
||||
|
||||
return ProductConfiguratorWidget;
|
||||
|
||||
});
|
||||
@@ -1141,6 +1141,7 @@ class SaleOrderLine(models.Model):
|
||||
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
|
||||
|
||||
product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict')
|
||||
product_template_id = fields.Many2one('product.template', string='Product Template', related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)])
|
||||
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', readonly=True, default=True)
|
||||
product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
|
||||
product_uom = fields.Many2one('uom.uom', string='Unit of Measure')
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
odoo.define('sale.product_configurator', function (require) {
|
||||
var relationalFields = require('web.relational_fields');
|
||||
var FieldsRegistry = require('web.field_registry');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* The sale.product_configurator widget is a simple widget extending FieldMany2One
|
||||
* It allows the development of configuration strategies in other modules through
|
||||
* widget extensions.
|
||||
*
|
||||
*
|
||||
* !!! WARNING !!!
|
||||
*
|
||||
* This widget is only designed for sale_order_line creation/updates.
|
||||
* !!! It should only be used on a product_product or product_template field !!!
|
||||
*/
|
||||
var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
events: _.extend({}, relationalFields.FieldMany2One.prototype.events, {
|
||||
'click .o_edit_product_configuration': '_onEditProductConfiguration'
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_render: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.mode === 'edit' && this.value &&
|
||||
(this._isConfigurableProduct() || this._isConfigurableLine())) {
|
||||
this._addConfigurationEditButton();
|
||||
} else {
|
||||
this.$('.o_edit_product_configuration').hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* If current product is configurable,
|
||||
* Show edit button (in Edit Mode) after the product/product_template
|
||||
*/
|
||||
_addConfigurationEditButton: function () {
|
||||
var $inputDropdown = this.$('.o_input_dropdown');
|
||||
|
||||
if ($inputDropdown.length !== 0 &&
|
||||
this.$('.o_edit_product_configuration').length === 0) {
|
||||
var $editConfigurationButton = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'fa fa-pencil btn btn-secondary o_edit_product_configuration',
|
||||
tabindex: '-1',
|
||||
draggable: false,
|
||||
'aria-label': _t('Edit Configuration'),
|
||||
title: _t('Edit Configuration')
|
||||
});
|
||||
|
||||
$inputDropdown.after($editConfigurationButton);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook to override with _onEditProductConfiguration
|
||||
* to know if edit pencil button has to be put next to the field
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isConfigurableProduct: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook to override with _onEditProductConfiguration
|
||||
* to know if edit pencil button has to be put next to the field
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isConfigurableLine: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Override catching changes on product_id or product_template_id.
|
||||
* Calls _onTemplateChange in case of product_template change.
|
||||
* Calls _onProductChange in case of product change.
|
||||
* Shouldn't be overridden by product configurators
|
||||
* or only to setup some data for further computation
|
||||
* before calling super.
|
||||
*
|
||||
* @override
|
||||
* @param {OdooEvent} ev
|
||||
* {boolean} ev.data.preventProductIdCheck prevent the product configurator widget
|
||||
* from looping forever when it needs to change the 'product_template_id'
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onFieldChanged: function (ev) {
|
||||
var self = this;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (ev.data.changes && !ev.data.preventProductIdCheck && ev.data.changes.product_template_id) {
|
||||
self._onTemplateChange(ev.data.changes.product_template_id.id, ev.data.dataPointID);
|
||||
} else if (ev.data.changes && ev.data.changes.product_id) {
|
||||
self._onProductChange(ev.data.changes.product_id.id, ev.data.dataPointID);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook for product_template based configurators
|
||||
* (product configurator, matrix, ...).
|
||||
*
|
||||
* @param {integer} productTemplateId
|
||||
* @param {String} dataPointID
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onTemplateChange: function (productTemplateId, dataPointId) {
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook for product_product based configurators
|
||||
* (event, rental, ...).
|
||||
* Should return
|
||||
* true if product has been configured through wizard or
|
||||
* the result of the super call for other wizard extensions
|
||||
* false if the product wasn't configurable through the wizard
|
||||
*
|
||||
* @param {integer} productId
|
||||
* @param {String} dataPointID
|
||||
* @returns {Promise<Boolean>} stopPropagation true if a suitable configurator has been found.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onProductChange: function (productId, dataPointId) {
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook for editing a configured line.
|
||||
* The button triggering this function is only shown in Edit mode,
|
||||
* when _isConfigurableProduct is True.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onEditProductConfiguration: function () {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Utilities for recordData conversion
|
||||
*/
|
||||
|
||||
/**
|
||||
* Will convert the values contained in the recordData parameter to
|
||||
* a list of '4' operations that can be passed as a 'default_' parameter.
|
||||
*
|
||||
* @param {Object} recordData
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_convertFromMany2Many: function (recordData) {
|
||||
if (recordData) {
|
||||
var convertedValues = [];
|
||||
_.each(recordData.res_ids, function (resId) {
|
||||
convertedValues.push([4, parseInt(resId)]);
|
||||
});
|
||||
|
||||
return convertedValues;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will convert the values contained in the recordData parameter to
|
||||
* a list of '0' or '4' operations (based on wether the record is already persisted or not)
|
||||
* that can be passed as a 'default_' parameter.
|
||||
*
|
||||
* @param {Object} recordData
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_convertFromOne2Many: function (recordData) {
|
||||
if (recordData) {
|
||||
var convertedValues = [];
|
||||
_.each(recordData.res_ids, function (resId) {
|
||||
if (isNaN(resId)) {
|
||||
_.each(recordData.data, function (record) {
|
||||
if (record.ref === resId) {
|
||||
convertedValues.push([0, 0, {
|
||||
attribute_value_id: record.data.attribute_value_id.data.id,
|
||||
custom_value: record.data.custom_value
|
||||
}]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
convertedValues.push([4, resId]);
|
||||
}
|
||||
});
|
||||
|
||||
return convertedValues;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
FieldsRegistry.add('product_configurator', ProductConfiguratorWidget);
|
||||
|
||||
return ProductConfiguratorWidget;
|
||||
|
||||
});
|
||||
@@ -217,4 +217,14 @@ label.css_attribute_color.css_not_available {
|
||||
.oe_striked_price {
|
||||
text-decoration: line-through;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.o_list_view {
|
||||
.o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) {
|
||||
.o_field_widget .o_edit_product_configuration {
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<script type="text/javascript" src="/sale/static/src/js/sale.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/tours/sale.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/account_reconciliation.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/product_configurator_widget.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="assets_frontend_inherit_sale" inherit_id="web.assets_frontend" name="Sale frontend assets">
|
||||
|
||||
@@ -418,7 +418,26 @@
|
||||
'default_lst_price': price_unit,
|
||||
'default_description_sale': name
|
||||
}"
|
||||
widget="product_configurator"
|
||||
/>
|
||||
<field name="product_template_id"
|
||||
string="Product"
|
||||
invisible="1"
|
||||
attrs="{
|
||||
'readonly': [('product_updatable', '=', False)],
|
||||
'required': [('display_type', '=', False)],
|
||||
}"
|
||||
options="{'no_open': True}"
|
||||
context="{
|
||||
'partner_id': parent.partner_id,
|
||||
'quantity': product_uom_qty,
|
||||
'pricelist': parent.pricelist_id,
|
||||
'uom':product_uom,
|
||||
'company_id': parent.company_id,
|
||||
'default_lst_price': price_unit,
|
||||
'default_description_sale': name
|
||||
}"
|
||||
widget="product_configurator"/>
|
||||
<field name="name" widget="section_and_note_text" optional="show"/>
|
||||
<field
|
||||
name="analytic_tag_ids"
|
||||
|
||||
@@ -7,6 +7,5 @@ from odoo import fields, models
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
product_template_id = fields.Many2one('product.template', string='Product Template', related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)])
|
||||
is_configurable_product = fields.Boolean('Is the product configurable?', related="product_template_id.has_configurable_attributes")
|
||||
product_template_attribute_value_ids = fields.Many2many(related='product_id.product_template_attribute_value_ids', readonly=True)
|
||||
|
||||
@@ -26,7 +26,7 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willStart: function (){
|
||||
willStart: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (this.initialState.data.product_template_id) {
|
||||
return this._configureProduct(
|
||||
@@ -67,7 +67,7 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
* @override
|
||||
*/
|
||||
_onButtonClicked: function (event) {
|
||||
if (event.stopPropagation){
|
||||
if (event.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
var attrs = event.data.attrs;
|
||||
@@ -77,7 +77,7 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
if (!this.$el
|
||||
.parents('.modal')
|
||||
.find('.o_sale_product_configurator_add')
|
||||
.hasClass('disabled')){
|
||||
.hasClass('disabled')) {
|
||||
this._handleAdd();
|
||||
}
|
||||
}
|
||||
@@ -92,15 +92,15 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var self = this;
|
||||
var product_id = event.data.changes.product_template_id.id;
|
||||
var productId = event.data.changes.product_template_id.id;
|
||||
|
||||
// check to prevent traceback when emptying the field
|
||||
if (!product_id) {
|
||||
if (!productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._configureProduct(event.data.changes.product_template_id.id)
|
||||
.then(function (){
|
||||
.then(function () {
|
||||
self.renderer.renderConfigurator(self.renderer.configuratorHtml);
|
||||
});
|
||||
},
|
||||
@@ -293,7 +293,7 @@ var ProductConfiguratorFormController = FormController.extend({
|
||||
* @param {Array} attributeValueIds
|
||||
*/
|
||||
_getAttributeValueIds: function (attributeValueIds) {
|
||||
if (!attributeValueIds || attributeValueIds.length === 0){
|
||||
if (!attributeValueIds || attributeValueIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(VariantMixin, {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (){
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.pricelistId = this.state.context.default_pricelist_id || 0;
|
||||
},
|
||||
|
||||
@@ -1,138 +1,103 @@
|
||||
odoo.define('sale_product_configurator.product_configurator', function (require) {
|
||||
var relationalFields = require('web.relational_fields');
|
||||
var FieldsRegistry = require('web.field_registry');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var ProductConfiguratorWidget = require('sale.product_configurator');
|
||||
|
||||
/**
|
||||
* The product configurator widget is a simple FieldMany2One that adds the capability
|
||||
* to configure a product_template_id using the product configurator wizard.
|
||||
* 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
|
||||
*
|
||||
* !!! It should only be used on a product_template_id field !!!
|
||||
*/
|
||||
var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
events: _.extend({}, relationalFields.FieldMany2One.prototype.events, {
|
||||
'click .o_edit_product_configuration': '_onEditProductConfiguration'
|
||||
}),
|
||||
|
||||
ProductConfiguratorWidget.include({
|
||||
/**
|
||||
* This method will check if the current product_template set on the SO line is configurable
|
||||
* -> If so, we add a 'Edit Configuration' button next to the dropdown.
|
||||
* Override of sale.product_configurator Hook
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var prom = this._super.apply(this, arguments);
|
||||
|
||||
var $inputDropdown = this.$('.o_input_dropdown');
|
||||
|
||||
if (this.recordData.is_configurable_product &&
|
||||
$inputDropdown.length !== 0 &&
|
||||
this.$('.o_edit_product_configuration').length === 0) {
|
||||
var $editConfigurationButton = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'fa fa-pencil btn btn-secondary o_edit_product_configuration',
|
||||
tabindex: '-1',
|
||||
draggable: false,
|
||||
'aria-label': _t('Edit Configuration'),
|
||||
title: _t('Edit Configuration')
|
||||
});
|
||||
|
||||
$inputDropdown.after($editConfigurationButton);
|
||||
}
|
||||
return prom;
|
||||
*/
|
||||
_isConfigurableProduct: function () {
|
||||
return this.recordData.is_configurable_product || this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is overridden to check to check if the product_template_id
|
||||
* needs configuration or not:
|
||||
* Set restoreProductTemplateId for further backtrack.
|
||||
*
|
||||
* - 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
|
||||
* @param {OdooEvent} event
|
||||
* {boolean} event.data.preventProductIdCheck prevent the product configurator widget
|
||||
* from looping forever when it needs to change the 'product_template_id'
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onFieldChanged: function (event) {
|
||||
var self = this;
|
||||
self.restoreProductTemplateId = self.recordData.product_template_id;
|
||||
_onFieldChanged: function (ev) {
|
||||
this.restoreProductTemplateId = this.recordData.product_template_id;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var $inputDropdown = this.$('.o_input_dropdown');
|
||||
if (event.data.changes.product_template_id
|
||||
&& $inputDropdown.length !== 0 &&
|
||||
this.$('.o_edit_product_configuration').length !== 0){
|
||||
this.$('.o_edit_product_configuration').remove();
|
||||
}
|
||||
|
||||
if (!event.data.changes.product_template_id
|
||||
|| event.data.preventProductIdCheck){
|
||||
return;
|
||||
}
|
||||
|
||||
var productTemplateId = event.data.changes.product_template_id.id;
|
||||
if (productTemplateId){
|
||||
this._rpc({
|
||||
model: 'product.template',
|
||||
method: 'get_single_product_variant',
|
||||
args: [
|
||||
productTemplateId
|
||||
]
|
||||
}).then(function (result){
|
||||
if (result){
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: event.data.dataPointID,
|
||||
changes: {
|
||||
product_id: {id: result.product_id},
|
||||
product_custom_attribute_value_ids: {
|
||||
operation: 'DELETE_ALL'
|
||||
}
|
||||
},
|
||||
onSuccess: function () {
|
||||
if (result.has_optional_products) {
|
||||
self._openProductConfigurator({
|
||||
configuratorMode: 'options',
|
||||
default_pricelist_id: self._getPricelistId(),
|
||||
default_product_template_id: productTemplateId
|
||||
},
|
||||
event.data.dataPointID
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self._onSimpleProductFound(result.product_id, event.data.dataPointID);
|
||||
} else {
|
||||
self._openProductConfigurator({
|
||||
configuratorMode: 'add',
|
||||
default_pricelist_id: self._getPricelistId(),
|
||||
default_product_template_id: productTemplateId
|
||||
},
|
||||
event.data.dataPointID
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hooking point for other modules
|
||||
*
|
||||
* @param {integer} productId
|
||||
* @param {string} dataPointID
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSimpleProductFound: function (productId, dataPointID) {},
|
||||
* 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;
|
||||
|
||||
return this._rpc({
|
||||
model: 'product.template',
|
||||
method: 'get_single_product_variant',
|
||||
args: [
|
||||
productTemplateId
|
||||
]
|
||||
}).then(function (result) {
|
||||
if (result) {
|
||||
self.trigger_up('field_changed', {
|
||||
dataPointID: dataPointId,
|
||||
changes: {
|
||||
product_id: {id: result.product_id},
|
||||
product_custom_attribute_value_ids: {
|
||||
operation: 'DELETE_ALL'
|
||||
}
|
||||
},
|
||||
onSuccess: function () {
|
||||
if (result.has_optional_products) {
|
||||
// VFE WARNING optional products and configurable product_id don't go well
|
||||
// together for the moment...
|
||||
self._openOptionsConfigurator(productTemplateId, dataPointId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self._openProductConfigurator({
|
||||
configuratorMode: 'add',
|
||||
default_pricelist_id: self._getPricelistId(),
|
||||
default_product_template_id: productTemplateId
|
||||
},
|
||||
dataPointId
|
||||
);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
// always returns true for the moment because no other configurator exists.
|
||||
});
|
||||
},
|
||||
|
||||
_openOptionsConfigurator: function (productTemplateId, dataPointId) {
|
||||
this._openProductConfigurator({
|
||||
configuratorMode: 'options',
|
||||
default_pricelist_id: this._getPricelistId(),
|
||||
default_product_template_id: productTemplateId
|
||||
},
|
||||
dataPointId
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the product configurator to allow configuring the product template
|
||||
@@ -150,7 +115,7 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
* the product_template is reset to its previous value if any.
|
||||
*
|
||||
* @param {Object} data various "default_" values
|
||||
* {string} data.configuratorMode 'add' or 'edit'.
|
||||
* {string} data.configuratorMode 'add' or 'edit' or 'options'.
|
||||
* @param {string} dataPointId
|
||||
*
|
||||
* @private
|
||||
@@ -160,7 +125,9 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
this.do_action('sale_product_configurator.sale_product_configurator_action', {
|
||||
additional_context: data,
|
||||
on_close: function (result) {
|
||||
if (result && result !== 'special'){
|
||||
if (result && result !== 'special') {
|
||||
// TODO if mode == 'options'
|
||||
// if no options chosen, don't trigger events...
|
||||
self._addProducts(result, dataPointId);
|
||||
} else {
|
||||
if (self.restoreProductTemplateId) {
|
||||
@@ -186,6 +153,13 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
* @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;
|
||||
}
|
||||
// If line has been set up through the product_configurator:
|
||||
this._openProductConfigurator({
|
||||
configuratorMode: 'edit',
|
||||
default_product_template_id: this.recordData.product_template_id.data.id,
|
||||
@@ -230,16 +204,22 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
preventProductIdCheck: true,
|
||||
changes: this._getMainProductChanges(result.mainProduct),
|
||||
onSuccess: function () {
|
||||
if (result.options) {
|
||||
var parentList = self.getParent();
|
||||
var parentList = self.getParent();
|
||||
if (result.options && result.options.length !== 0) {
|
||||
self.trigger_up('add_record', {
|
||||
context: self._productsToRecords(result.options),
|
||||
forceEditable: 'bottom',
|
||||
allowWarning: true,
|
||||
onSuccess: function (){
|
||||
onSuccess: function () {
|
||||
// Leave edit mode of the last line added.
|
||||
parentList.unselectRow();
|
||||
}
|
||||
});
|
||||
|
||||
} else if (!self._isConfigurableLine()) {
|
||||
// Leave edit mode of current line if no other wizard
|
||||
// will be opened by the field_changed event.
|
||||
parentList.unselectRow();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -296,13 +276,13 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
var noVariantAttributeValues = mainProduct.no_variant_attribute_values;
|
||||
var noVariantCommands = [{operation: 'DELETE_ALL'}];
|
||||
if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) {
|
||||
var res_ids = _.map(noVariantAttributeValues, function (noVariantValue) {
|
||||
var resIds = _.map(noVariantAttributeValues, function (noVariantValue) {
|
||||
return {id: parseInt(noVariantValue.value)};
|
||||
});
|
||||
|
||||
noVariantCommands.push({
|
||||
operation: 'ADD_M2M',
|
||||
ids: res_ids
|
||||
ids: resIds
|
||||
});
|
||||
}
|
||||
|
||||
@@ -334,7 +314,7 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
*/
|
||||
_productsToRecords: function (products) {
|
||||
var records = [];
|
||||
_.each(products, function (product){
|
||||
_.each(products, function (product) {
|
||||
var record = {
|
||||
default_product_id: product.product_id,
|
||||
default_product_template_id: product.product_template_id,
|
||||
@@ -370,65 +350,9 @@ var ProductConfiguratorWidget = relationalFields.FieldMany2One.extend({
|
||||
});
|
||||
|
||||
return records;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will convert the values contained in the recordData parameter to
|
||||
* a list of '4' operations that can be passed as a 'default_' parameter.
|
||||
*
|
||||
* @param {Object} recordData
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_convertFromMany2Many: function (recordData) {
|
||||
if (recordData) {
|
||||
var convertedValues = [];
|
||||
_.each(recordData.res_ids, function (res_id) {
|
||||
convertedValues.push([4, parseInt(res_id)]);
|
||||
});
|
||||
|
||||
return convertedValues;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will convert the values contained in the recordData parameter to
|
||||
* a list of '0' or '4' operations (based on wether the record is already persisted or not)
|
||||
* that can be passed as a 'default_' parameter.
|
||||
*
|
||||
* @param {Object} recordData
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_convertFromOne2Many: function (recordData) {
|
||||
if (recordData) {
|
||||
var convertedValues = [];
|
||||
_.each(recordData.res_ids, function (res_id) {
|
||||
if (isNaN(res_id)){
|
||||
_.each(recordData.data, function (record) {
|
||||
if (record.ref === res_id) {
|
||||
convertedValues.push([0, 0, {
|
||||
attribute_value_id: record.data.attribute_value_id.data.id,
|
||||
custom_value: record.data.custom_value
|
||||
}]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
convertedValues.push([4, res_id]);
|
||||
}
|
||||
});
|
||||
|
||||
return convertedValues;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
FieldsRegistry.add('product_configurator', ProductConfiguratorWidget);
|
||||
|
||||
return ProductConfiguratorWidget;
|
||||
|
||||
});
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.o_list_view {
|
||||
.o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) {
|
||||
.o_field_widget .o_edit_product_configuration {
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" name="sale_product_configurator assets backend">
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/sale_product_configurator/static/src/scss/product_configurator_widget.scss"/>
|
||||
</xpath>
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/sale_product_configurator/static/src/js/tours/sale_product_configurator.js"></script>
|
||||
<script type="text/javascript" src="/sale/static/src/js/variant_mixin.js"></script>
|
||||
|
||||
@@ -20,26 +20,10 @@
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree/field[@name='product_id']" position="after">
|
||||
<field
|
||||
name="product_template_id"
|
||||
string="Product"
|
||||
options="{'no_open': True}"
|
||||
attrs="{
|
||||
'readonly': [('product_updatable', '=', False)],
|
||||
'required': [('display_type', '=', False)],
|
||||
}"
|
||||
context="{
|
||||
'partner_id': parent.partner_id,
|
||||
'quantity': product_uom_qty,
|
||||
'pricelist': parent.pricelist_id,
|
||||
'uom':product_uom,
|
||||
'company_id': parent.company_id,
|
||||
'default_lst_price': price_unit,
|
||||
'default_description_sale': name
|
||||
}"
|
||||
widget="product_configurator"
|
||||
/>
|
||||
<xpath expr="//tree/field[@name='product_template_id']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//tree/field[@name='product_template_id']" position="after">
|
||||
<field name="product_template_attribute_value_ids" invisible="1" />
|
||||
<field name="product_custom_attribute_value_ids" invisible="1" >
|
||||
<tree>
|
||||
|
||||
Reference in New Issue
Block a user