[IMP] web: fix default sequence for list with handle
The sequence had a static default value, but when there is an handle, it's important that new items are added at the top or the bottom of the list. This is a rare case where the defaul_get from the server will be ignored by the view for a certain field. This will work: - both in editable mode (inline) and in dialog mode, - both for X2Many and standalone list.
This commit is contained in:
@@ -70,6 +70,13 @@
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="base.group_no_one"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<!--
|
||||
We need the sequence field to be here
|
||||
because we want to be able to overwrite the default sequence value in the JS
|
||||
in order for new lines to be added at the correct position.
|
||||
NOTE: at some point we want to fix this in the framework so that an invisible field is not required.
|
||||
-->
|
||||
<field name="sequence" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/>
|
||||
@@ -686,7 +693,7 @@
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a credit note
|
||||
</p><p>
|
||||
Note that the easiest way to create a credit note is to do it directly
|
||||
Note that the easiest way to create a credit note is to do it directly
|
||||
from the customer invoice.
|
||||
</p>
|
||||
</field>
|
||||
|
||||
@@ -271,6 +271,13 @@
|
||||
<div name="discount" groups="sale.group_discount_per_so_line">
|
||||
<field name="discount" class="oe_inline"/> %%
|
||||
</div>
|
||||
<!--
|
||||
We need the sequence field to be here
|
||||
because we want to be able to overwrite the default sequence value in the JS
|
||||
in order for new lines to be added at the correct position.
|
||||
NOTE: at some point we want to fix this in the framework so that an invisible field is not required.
|
||||
-->
|
||||
<field name="sequence" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="tax_id" widget="many2many_tags" options="{'no_create': True}" context="{'search_view_ref': 'account.account_tax_view_search'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
|
||||
@@ -551,10 +558,10 @@
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No orders to upsell found
|
||||
</p><p>
|
||||
These are orders with products invoiced based on ordered quantities,
|
||||
These are orders with products invoiced based on ordered quantities,
|
||||
in the case you have delivered more than what was ordered.
|
||||
</p><p>
|
||||
A typical example is the pre-paid hours of service,
|
||||
A typical example is the pre-paid hours of service,
|
||||
where you want to sell extra hours to the customer
|
||||
because the initial hours have already been used.
|
||||
</p>
|
||||
|
||||
@@ -1293,12 +1293,14 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
var self = this;
|
||||
var data = ev.data || {};
|
||||
|
||||
// we don't want interference with the components upstream.
|
||||
ev.stopPropagation();
|
||||
if (this.editable) {
|
||||
if (!this.activeActions.create) {
|
||||
if (ev.data.onFail) {
|
||||
ev.data.onFail();
|
||||
if (data.onFail) {
|
||||
data.onFail();
|
||||
}
|
||||
} else if (!this.creatingRecord) {
|
||||
this.creatingRecord = true;
|
||||
|
||||
@@ -183,6 +183,7 @@ var BasicModel = AbstractModel.extend({
|
||||
fields: list.fields,
|
||||
fieldsInfo: list.fieldsInfo,
|
||||
parentID: list.id,
|
||||
position: position,
|
||||
viewType: list.viewType,
|
||||
};
|
||||
return this._makeDefaultRecord(list.model, params).then(function (id) {
|
||||
@@ -304,6 +305,104 @@ var BasicModel = AbstractModel.extend({
|
||||
return record.id;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Compute the default value that the handle field should take.
|
||||
* We need to compute this in order for new lines to be added at the correct position.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} listID
|
||||
* @param {string} position
|
||||
* @return {Object} empty object if no overrie has to be done, or:
|
||||
* field: the name of the field to override,
|
||||
* value: the value to use for that field
|
||||
*/
|
||||
_computeOverrideDefaultFields: function (listID, position) {
|
||||
var list = this.localData[listID];
|
||||
var handleField;
|
||||
|
||||
// Here listID is actually just parentID, it's not yet confirmed
|
||||
// to be a list.
|
||||
// If we are not in the case that interests us,
|
||||
// listID will be undefined and this check will work.
|
||||
if (!list) {
|
||||
return {};
|
||||
}
|
||||
|
||||
position = position || 'bottom';
|
||||
|
||||
// Let's find if there is a field with handle.
|
||||
if (!list.fieldsInfo) {
|
||||
return {};
|
||||
}
|
||||
for (var field in list.fieldsInfo.list) {
|
||||
if (list.fieldsInfo.list[field].widget === 'handle') {
|
||||
handleField = field;
|
||||
break;
|
||||
// If there are 2 handle fields on the same list,
|
||||
// we take the first one we find.
|
||||
// And that will be alphabetically on the field name...
|
||||
}
|
||||
}
|
||||
|
||||
if (!handleField) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// We don't want to override the default value
|
||||
// if the list is not ordered by the handle field.
|
||||
var isOrderedByHandle = list.orderedBy
|
||||
&& list.orderedBy.length
|
||||
&& list.orderedBy[0].asc === true
|
||||
&& list.orderedBy[0].name === handleField;
|
||||
|
||||
if (!isOrderedByHandle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// We compute the list (get) to apply the pending changes before doing our work,
|
||||
// otherwise new lines might not be taken into account.
|
||||
// We use raw: true because we only need to load the first level of relation.
|
||||
var computedList = this.get(list.id, {raw: true});
|
||||
|
||||
// We don't need to worry about the position of a new line if the list is empty.
|
||||
if (!computedList || !computedList.data || !computedList.data.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// If there are less elements in the list than the limit of
|
||||
// the page then take the index of the last existing line.
|
||||
|
||||
// If the button is at the top, we want the new element on
|
||||
// the first line of the page.
|
||||
|
||||
// If the button is at the bottom, we want the new element
|
||||
// after the last line of the page
|
||||
// (= theorically it will be the first element of the next page).
|
||||
|
||||
// We ignore list.offset because computedList.data
|
||||
// will only have the current page elements.
|
||||
|
||||
var index = Math.min(
|
||||
computedList.data.length - 1,
|
||||
position !== 'top' ? list.limit - 1 : 0
|
||||
);
|
||||
|
||||
// This positioning will almost be correct. There might just be
|
||||
// an issue if several other lines have the same handleFieldValue.
|
||||
|
||||
// TODO ideally: if there is an element with the same handleFieldValue,
|
||||
// that one and all the following elements must be incremented
|
||||
// by 1 (at least until there is a gap in the numbering).
|
||||
|
||||
// We don't do it now because it's not an important case.
|
||||
var handleFieldValue = computedList.data[index].data[handleField];
|
||||
handleFieldValue = position !== 'top' ? handleFieldValue : handleFieldValue - 1;
|
||||
|
||||
return {
|
||||
field: handleField,
|
||||
value: handleFieldValue,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Delete a list of records, then, if the records have a parent, reload it.
|
||||
*
|
||||
@@ -1251,15 +1350,16 @@ var BasicModel = AbstractModel.extend({
|
||||
*/
|
||||
_addX2ManyDefaultRecord: function (list, options) {
|
||||
var self = this;
|
||||
var position = options && options.position || 'top';
|
||||
var params = {
|
||||
context: this._getContext(list),
|
||||
fields: list.fields,
|
||||
fieldsInfo: list.fieldsInfo,
|
||||
parentID: list.id,
|
||||
position: position,
|
||||
viewType: list.viewType,
|
||||
};
|
||||
return this._makeDefaultRecord(list.model, params).then(function (id) {
|
||||
var position = options && options.position || 'top';
|
||||
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
|
||||
var record = self.localData[id];
|
||||
list._cache[record.res_id] = id;
|
||||
@@ -3436,7 +3536,7 @@ var BasicModel = AbstractModel.extend({
|
||||
_makeDefaultRecord: function (modelName, params) {
|
||||
var self = this;
|
||||
|
||||
var determineExtraFields = function() {
|
||||
var determineExtraFields = function () {
|
||||
// Fields that are present in the originating view, that need to be initialized
|
||||
// Hence preventing their value to crash when getting back to the originating view
|
||||
var parentRecord = self.localData[params.parentID];
|
||||
@@ -3446,12 +3546,12 @@ var BasicModel = AbstractModel.extend({
|
||||
return [];
|
||||
|
||||
var fieldsFromOrigin = _.filter(Object.keys(originView[parentRecord.viewType]),
|
||||
function(fieldname) {
|
||||
function (fieldname) {
|
||||
return params.fields[fieldname] !== undefined;
|
||||
});
|
||||
|
||||
return fieldsFromOrigin;
|
||||
}
|
||||
};
|
||||
|
||||
var fieldNames = Object.keys(params.fieldsInfo[params.viewType]);
|
||||
var fields_key = _.without(fieldNames, '__last_update');
|
||||
@@ -3465,6 +3565,7 @@ var BasicModel = AbstractModel.extend({
|
||||
context: params.context,
|
||||
})
|
||||
.then(function (result) {
|
||||
|
||||
var record = self._makeDataPoint({
|
||||
modelName: modelName,
|
||||
fields: params.fields,
|
||||
@@ -3475,6 +3576,20 @@ var BasicModel = AbstractModel.extend({
|
||||
viewType: params.viewType,
|
||||
});
|
||||
|
||||
// We want to overwrite the default value of the handle field (if any),
|
||||
// in order for new lines to be added at the correct position.
|
||||
// -> This is a rare case where the defaul_get from the server
|
||||
// will be ignored by the view for a certain field (usually "sequence").
|
||||
|
||||
var overrideDefaultFields = self._computeOverrideDefaultFields(
|
||||
params.parentID,
|
||||
params.position
|
||||
);
|
||||
|
||||
if (overrideDefaultFields) {
|
||||
result[overrideDefaultFields.field] = overrideDefaultFields.value;
|
||||
}
|
||||
|
||||
return self.applyDefaultValues(record.id, result, {fieldNames: _.union(fieldNames, extraFields)})
|
||||
.then(function () {
|
||||
var def = $.Deferred();
|
||||
|
||||
@@ -477,7 +477,7 @@ ListRenderer.include({
|
||||
},
|
||||
/**
|
||||
* The renderer needs to support reordering lines. This is only active in
|
||||
* edit mode. The hasHandle attribute is used when there is a sequence
|
||||
* edit mode. The handleField attribute is set when there is a sequence
|
||||
* widget.
|
||||
*
|
||||
* @override
|
||||
@@ -485,7 +485,7 @@ ListRenderer.include({
|
||||
*/
|
||||
_renderBody: function () {
|
||||
var $body = this._super();
|
||||
if (this.hasHandle) {
|
||||
if (this.handleField) {
|
||||
$body.sortable({
|
||||
axis: 'y',
|
||||
items: '> tr.o_data_row',
|
||||
|
||||
@@ -50,8 +50,9 @@ var ListRenderer = BasicRenderer.extend({
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.hasHandle = false;
|
||||
this.handleField = 'sequence';
|
||||
// This attribute lets us know if there is a handle widget on a field,
|
||||
// and on which field it is set.
|
||||
this.handleField = null;
|
||||
this._processColumns(params.columnInvisibleFields || {});
|
||||
this.rowDecorations = _.chain(this.arch.attrs)
|
||||
.pick(function (value, key) {
|
||||
@@ -181,7 +182,6 @@ var ListRenderer = BasicRenderer.extend({
|
||||
*/
|
||||
_processColumns: function (columnInvisibleFields) {
|
||||
var self = this;
|
||||
self.hasHandle = false;
|
||||
self.handleField = null;
|
||||
this.columns = _.reject(this.arch.children, function (c) {
|
||||
var reject = c.attrs.modifiers.column_invisible;
|
||||
@@ -191,7 +191,6 @@ var ListRenderer = BasicRenderer.extend({
|
||||
reject = columnInvisibleFields[c.attrs.name];
|
||||
}
|
||||
if (!reject && c.attrs.widget === 'handle') {
|
||||
self.hasHandle = true;
|
||||
self.handleField = c.attrs.name;
|
||||
}
|
||||
return reject;
|
||||
|
||||
@@ -9074,6 +9074,137 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many with sequence field, override default_get, bottom when inline', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [3, 2, 1];
|
||||
|
||||
this.data.turtle.fields.turtle_int.default = 10;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="turtle_int" widget="handle"/>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// starting condition
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
|
||||
|
||||
// click add a new line
|
||||
// save the record
|
||||
// check line is at the correct place
|
||||
|
||||
var inputText = 'ninja';
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_input[name="turtle_foo"]').val(inputText).trigger('input');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many with sequence field, override default_get, top when inline', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [3, 2, 1];
|
||||
|
||||
this.data.turtle.fields.turtle_int.default = 10;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="turtle_int" widget="handle"/>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// starting condition
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
|
||||
|
||||
// click add a new line
|
||||
// save the record
|
||||
// check line is at the correct place
|
||||
|
||||
var inputText = 'ninja';
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_input[name="turtle_foo"]').val(inputText).trigger('input');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), inputText + "blipyopkawa");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many with sequence field, override default_get, bottom when popup', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.records[0].turtles = [3, 2, 1];
|
||||
|
||||
this.data.turtle.fields.turtle_int.default = 10;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree>' +
|
||||
'<field name="turtle_int" widget="handle"/>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
// NOTE: at some point we want to fix this in the framework so that an invisible field is not required.
|
||||
'<field name="turtle_int" invisible="1"/>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
// starting condition
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
|
||||
|
||||
// click add a new line
|
||||
// save the record
|
||||
// check line is at the correct place
|
||||
|
||||
var inputText = 'ninja';
|
||||
$('.o_field_x2many_list_row_add a').click();
|
||||
$('.o_input[name="turtle_foo"]').val(inputText).trigger('input');
|
||||
$('.modal .modal-footer .btn-primary:first').click();
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
|
||||
|
||||
$('.o_form_button_save').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many with sequence field, fetch name_get from empty list', function (assert) {
|
||||
// There was a bug where a RPC would fail because no route was set.
|
||||
// The scenario is:
|
||||
|
||||
@@ -3613,6 +3613,39 @@ QUnit.module('Views', {
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list with handle field, override default_get, bottom when inline', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.foo.fields.int_field.default = 10;
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<tree editable="bottom" default_order="int_field">'
|
||||
+ '<field name="int_field" widget="handle"/>'
|
||||
+ '<field name="foo"/>'
|
||||
+'</tree>',
|
||||
});
|
||||
|
||||
// starting condition
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipblipyopgnap");
|
||||
|
||||
// click add a new line
|
||||
// save the record
|
||||
// check line is at the correct place
|
||||
|
||||
var inputText = 'ninja';
|
||||
$('.o_list_button_add').click();
|
||||
list.$('.o_input[name="foo"]').val(inputText).trigger('input');
|
||||
$('.o_list_button_add').click();
|
||||
|
||||
assert.strictEqual($('.o_data_cell').text(), "blipblipyopgnap" + inputText);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1478,9 +1478,11 @@ order.
|
||||
|
||||
|
||||
- handle (HandleWidget)
|
||||
This field's job is to be displayed as a *handle* in a list view, and allow
|
||||
This field's job is to be displayed as a *handle* in a list view, and allows
|
||||
reordering the various records by drag and dropping lines.
|
||||
|
||||
.. warning:: Having more than one field with a handle widget on the same list is not supported.
|
||||
|
||||
- Supported field types: *integer*
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user