[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:
Sébastien Theys
2018-06-29 10:39:42 +02:00
parent 217fee2636
commit b2c1d5ede2
9 changed files with 312 additions and 16 deletions
@@ -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>
+9 -2
View File
@@ -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();
});
});
});
+3 -1
View File
@@ -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*