diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js
new file mode 100644
index 00000000000..30bd5c4b22a
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_singleton_list_controller.js
@@ -0,0 +1,65 @@
+odoo.define('stock.SingletonListController', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ListController = require('web.ListController');
+
+var _t = core._t;
+
+/**
+ * The purpose of this override is to avoid to have two or more similar records
+ * in the list view.
+ *
+ * It's used in quant list view, a list editable where when you create a new
+ * line about a quant who already exists, we want to update the existing one
+ * instead of create a new one, and then we don't want to have two similar line
+ * in the list view, so we refresh it.
+ */
+
+var SingletonListController = ListController.extend({
+ /**
+ * @override
+ */
+ _confirmSave: function (id) {
+ var newRecord = this.model.localData[id];
+ var model = newRecord.model;
+ var res_id = newRecord.res_id;
+
+ var findSimilarRecords = function (record) {
+ if ((record.groupedBy && record.groupedBy.length > 0) || record.data.length) {
+ var recordsToReturn = [];
+ for (var i in record.data) {
+ var foundRecords = findSimilarRecords(record.data[i]);
+ recordsToReturn = recordsToReturn.concat(foundRecords || []);
+ }
+ return recordsToReturn;
+ } else {
+ if (record.res_id === res_id && record.model === model) {
+ if (record.count === 0){
+ return [record];
+ }
+ else if (record.ref && record.ref.indexOf('virtual') !== -1) {
+ return [record];
+ }
+ }
+ }
+ };
+
+ var handle = this.model.get(this.handle);
+ var similarRecords = findSimilarRecords(handle);
+
+ if (similarRecords.length > 1) {
+ var notification = _t("You tried to create a record who already exists."+
+ "
This last one has been modified instead.");
+ this.do_notify(_t("This record already exists."), notification);
+ return this.reload();
+ }
+ else {
+ return this._super(id);
+ }
+ },
+});
+
+return SingletonListController;
+
+});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_view.js b/addons/stock/static/src/js/inventory_singleton_list_view.js
new file mode 100644
index 00000000000..1f6fc30e483
--- /dev/null
+++ b/addons/stock/static/src/js/inventory_singleton_list_view.js
@@ -0,0 +1,18 @@
+odoo.define('stock.SingletonListView', function (require) {
+'use strict';
+
+var ListView = require('web.ListView');
+var SingletonListController = require('stock.SingletonListController');
+var viewRegistry = require('web.view_registry');
+
+var SingletonListView = ListView.extend({
+ config: _.extend({}, ListView.prototype.config, {
+ Controller: SingletonListController,
+ }),
+});
+
+viewRegistry.add('singleton_list', SingletonListView);
+
+return SingletonListView;
+
+});
diff --git a/addons/stock/static/tests/singleton_list_tests.js b/addons/stock/static/tests/singleton_list_tests.js
new file mode 100644
index 00000000000..21886429b42
--- /dev/null
+++ b/addons/stock/static/tests/singleton_list_tests.js
@@ -0,0 +1,254 @@
+odoo.define('web.singleton_list_tests', function (require) {
+"use strict";
+
+var core = require('web.core');
+var SingletonListView = require('stock.SingletonListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ person: {
+ fields: {
+ name: {string: "Name", type: "char"},
+ age: {string: "Age", type: "integer"},
+ job: {string: "Profession", type: "char"},
+ },
+ records: [
+ {id: 1, name: 'Daniel Fortesque', age: 32, job: 'Soldier'},
+ {id: 2, name: 'Samuel Oak', age: 64, job: 'Professor'},
+ {id: 3, name: 'Leto II Atreides', age: 128, job: 'Emperor'},
+ ]
+ },
+ };
+ this.mockRPC = function (route, args) {
+ if (route === '/web/dataset/call_kw/person/create') {
+ var name = args.args[0].name;
+ var age = args.args[0].age;
+ var job = args.args[0].job;
+ for (var d of this.data.person.records) {
+ if (d.name === name) {
+ d.age = age;
+ d.job = job;
+ return Promise.resolve(d.id);
+ }
+ }
+ }
+ return this._super.apply(this, arguments);
+ };
+ }
+}, function () {
+
+ QUnit.module('SingletonListView');
+
+ QUnit.test('Create new record correctly', async function (assert) {
+ assert.expect(2);
+
+ var list = await createView({
+ View: SingletonListView,
+ model: 'person',
+ data: this.data,
+ arch: ''+
+ ''+
+ ''+
+ '',
+ mockRPC: this.mockRPC,
+ });
+ // Checks we have initially 3 records
+ assert.containsN(list, '.o_data_row', 3, "should have 3 records");
+
+ // Creates a new line...
+ await testUtils.dom.click($('.o_list_button_add'));
+ // ... and fills fields with new values
+ var $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Bilou');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '24');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.dom.click($('.o_list_button_save'));
+
+ // Checks new record is in the list
+ assert.containsN(list, '.o_data_row', 4, "should now have 4 records");
+ list.destroy();
+ });
+
+ QUnit.test('Don\'t duplicate record', async function (assert) {
+ assert.expect(3);
+
+ var list = await createView({
+ View: SingletonListView,
+ model: 'person',
+ data: this.data,
+ arch: ''+
+ ''+
+ ''+
+ '',
+ mockRPC: this.mockRPC,
+ });
+ // Checks we have initially 3 records
+ assert.containsN(list, '.o_data_row', 3, "should have 3 records");
+
+ // Creates a new line...
+ await testUtils.dom.click($('.o_list_button_add'));
+ // ... and fills fields with already existing value
+ var $input = $('.o_selected_row input[name=name]');
+ var name = 'Samuel Oak';
+ await testUtils.fields.editInput($input, name);
+ await testUtils.fields.triggerKeydown($input, 'tab');
+
+ $input = $('.o_selected_row input[name=age]');
+ var age = '72';
+ await testUtils.fields.editInput($input, age);
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.dom.click($('.o_list_button_save'));
+
+ // Checks we have still only 3 records...
+ assert.containsN(list, '.o_data_row', 3, "should now have 4 records");
+ // ... and verify modification was occured.
+ var nameField = list.$('td[title="' + name + '"]');
+ var ageField = nameField.parent().find('.o_list_number');
+ assert.strictEqual(ageField.text(), age, "The age field must be updated");
+ list.destroy();
+ });
+
+ QUnit.test('Refresh the list only when needed', async function (assert) {
+ assert.expect(3);
+
+ var refresh_count = 0;
+ var list = await createView({
+ View: SingletonListView,
+ model: 'person',
+ data: this.data,
+ arch: ''+
+ ''+
+ ''+
+ '',
+ mockRPC: this.mockRPC,
+ });
+ list.realReload = list.reload;
+ list.reload = function () {
+ refresh_count++;
+ return this.realReload();
+ };
+ // Modify Record
+ await testUtils.dom.click(list.$('.o_data_row:nth-child(2) > .o_list_number'));
+ var $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '70');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.dom.click($('.o_list_button_save'));
+ assert.strictEqual(refresh_count, 0, "don't refresh when edit existing line");
+
+ // Add existing record
+ await testUtils.dom.click($('.o_list_button_add'));
+ $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Leto II Atreides');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '800');
+ await testUtils.dom.click($('.o_list_button_save'));
+ assert.strictEqual(refresh_count, 1, "refresh after tried to create an existing record");
+
+ // Add new record
+ await testUtils.dom.click($('.o_list_button_add'));
+ $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Valentin Cognito');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '37');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.dom.click($('.o_list_button_save'));
+ assert.strictEqual(refresh_count, 1, "don't refresh when create entirely new record");
+
+ list.destroy();
+ });
+
+ QUnit.test('Work in grouped list', async function (assert) {
+ assert.expect(6);
+
+ var refresh_count = 0;
+ var list = await createView({
+ View: SingletonListView,
+ model: 'person',
+ data: this.data,
+ arch: ''+
+ ''+
+ ''+
+ ''+
+ '',
+ mockRPC: this.mockRPC,
+ groupBy: ['job'],
+ });
+ list.realReload = list.reload;
+ list.reload = function () {
+ refresh_count++;
+ return this.realReload();
+ };
+ // Opens 'Professor' group
+ await testUtils.dom.click(list.$('.o_group_header:nth-child(2)'));
+
+ // Creates a new record...
+ await testUtils.dom.click(list.$('.o_add_record_row a'));
+ var $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Del Tutorial');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '32');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ await testUtils.dom.click($('.o_list_button_save'));
+ // ... then checks the list didn't refresh
+ assert.strictEqual(refresh_count, 0,
+ "don't refresh when creating new record");
+
+ // Creates an existing record in same group...
+ await testUtils.dom.click(list.$('.o_add_record_row a'));
+ var $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Samuel Oak');
+ await testUtils.dom.click($('.o_list_button_save'));
+ // ... then checks the list has been refreshed
+ assert.strictEqual(refresh_count, 1,
+ "refresh when try to create an existing record");
+
+ // Creates an existing but not displayed record...
+ await testUtils.dom.click(list.$('.o_add_record_row a'));
+ var $input = $('.o_selected_row input[name=name]');
+ await testUtils.fields.editInput($input, 'Daniel Fortesque');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '55');
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ $input = $('.o_selected_row input[name=job]');
+ await testUtils.fields.editInput($input, 'Soldier');
+ await testUtils.dom.click($('.o_list_button_save'));
+ // .. then checks the list didn't refresh
+ assert.strictEqual(refresh_count, 1,
+ "don't refresh when creating an existing record but this record " +
+ "isn't present in the view");
+
+ // Opens 'Soldier' group
+ await testUtils.dom.click(list.$('.o_group_header:nth-child(1)').first());
+ // Checks the record has been correctly updated
+ var ageCell = $('tr.o_data_row td.o_list_number').first();
+ assert.strictEqual(ageCell.text(), "55",
+ "age of the record must be updated");
+ // Edits the freshly created record...
+ await testUtils.dom.click(list.$('tr.o_data_row td.o_list_number').eq(1));
+ $input = $('.o_selected_row input[name=age]');
+ await testUtils.fields.editInput($input, '66');
+ await testUtils.dom.click($('.o_list_button_save'));
+ // ... then checks the list and data have been refreshed
+ assert.strictEqual(refresh_count, 2,
+ "refresh when try to create an existing record present in the view");
+ ageCell = $('tr.o_data_row td.o_list_number').first();
+ assert.strictEqual(ageCell.text(), "66",
+ "age of the record must be updated");
+
+ list.destroy();
+ });
+});
+
+});
diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml
index 38286c6bff3..d14a4a2add8 100644
--- a/addons/stock/views/stock_quant_views.xml
+++ b/addons/stock/views/stock_quant_views.xml
@@ -75,7 +75,7 @@
+ create="1" edit="1" js_class="singleton_list">
+
+
@@ -12,6 +14,12 @@
+
+
+
+
+
+
The initial demand has been updated.