diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index a73a8a57d00..505e519f8b5 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -104,15 +104,16 @@
],
'web.assets_backend': [
'stock/static/src/**/*.js',
- 'stock/static/src/views/**/*.js',
'stock/static/src/scss/forecast_widget.scss',
'stock/static/src/scss/stock_empty_screen.scss',
+ 'stock/static/src/views/**/*.js',
+ 'stock/static/src/views/**/**.scss',
],
'web.assets_tests': [
'stock/static/tests/tours/stock_report_tests.js',
],
'web.qunit_suite_tests': [
- 'stock/static/tests/singleton_list_tests.js',
+ 'stock/static/tests/inventory_report_list_tests.js',
'stock/static/tests/popover_widget_tests.js',
'stock/static/tests/lazy_column_list_tests.js',
'stock/static/tests/stock_traceability_report_backend_tests.js',
diff --git a/addons/stock/static/src/js/inventory_report_list_controller.js b/addons/stock/static/src/js/inventory_report_list_controller.js
deleted file mode 100644
index 6bbf661b2d4..00000000000
--- a/addons/stock/static/src/js/inventory_report_list_controller.js
+++ /dev/null
@@ -1,61 +0,0 @@
-odoo.define('stock.InventoryReportListController', function (require) {
-"use strict";
-
-var ListController = require('web.ListController');
-
-var InventoryReportListController = ListController.extend({
- buttons_template: 'StockInventoryReport.Buttons',
-
- // -------------------------------------------------------------------------
- // Public
- // -------------------------------------------------------------------------
-
- init: function (parent, model, renderer, params) {
- this.context = renderer.state.getContext();
- return this._super.apply(this, arguments);
- },
-
- /**
- * @override
- */
- renderButtons: function ($node) {
- this._super.apply(this, arguments);
- if (this.context.no_at_date) {
- this.$buttons.find('button.o_button_at_date').hide();
- }
- this.$buttons.on('click', '.o_button_at_date', this._onOpenWizard.bind(this));
- },
-
- // -------------------------------------------------------------------------
- // Handlers
- // -------------------------------------------------------------------------
-
- /**
- * Handler called when the user clicked on the 'Valuation at Date' button.
- * Opens wizard to display, at choice, the products inventory or a computed
- * inventory at a given date.
- */
- _onOpenWizard: function () {
- var state = this.model.get(this.handle, {raw: true});
- var stateContext = state.getContext();
- var context = {
- active_model: this.modelName,
- };
- if (stateContext.default_product_id) {
- context.product_id = stateContext.default_product_id;
- } else if (stateContext.product_tmpl_id) {
- context.product_tmpl_id = stateContext.product_tmpl_id;
- }
- this.do_action({
- res_model: 'stock.quantity.history',
- views: [[false, 'form']],
- target: 'new',
- type: 'ir.actions.act_window',
- context: context,
- });
- },
-});
-
-return InventoryReportListController;
-
-});
diff --git a/addons/stock/static/src/js/inventory_report_list_view.js b/addons/stock/static/src/js/inventory_report_list_view.js
deleted file mode 100644
index 494dd838b73..00000000000
--- a/addons/stock/static/src/js/inventory_report_list_view.js
+++ /dev/null
@@ -1,19 +0,0 @@
-odoo.define('stock.InventoryReportListView', function (require) {
-"use strict";
-
-var ListView = require('web.ListView');
-var InventoryReportListController = require('stock.InventoryReportListController');
-var viewRegistry = require('web.view_registry');
-
-
-var InventoryReportListView = ListView.extend({
- config: _.extend({}, ListView.prototype.config, {
- Controller: InventoryReportListController,
- }),
-});
-
-viewRegistry.add('inventory_report_list', InventoryReportListView);
-
-return InventoryReportListView;
-
-});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js
deleted file mode 100644
index c36ad06a04c..00000000000
--- a/addons/stock/static/src/js/inventory_singleton_list_controller.js
+++ /dev/null
@@ -1,141 +0,0 @@
-odoo.define('stock.SingletonListController', function (require) {
-"use strict";
-
-var core = require('web.core');
-var InventoryReportListController = require('stock.InventoryReportListController');
-var session = require('web.session');
-
-var _t = core._t;
-
-/**
- * The purpose of this override has 2 purposes:
- *
- * 1. Avoid having two or more similar records in the list view.
- * It is expected to be used in quant list views, when the list is editable
- * and allows new line creation. Checks for whether newly created quant
- * already exists and if it does => update the existing quant instead of the
- * newly created one, and then we refresh the view to avoid having 2 similar lines.
- *
- * 2. Support Inventory Adjustments (i.e. inventory counts) view.
- * Specifically adds necessary buttons (or hides them when appropriate) and
- * autosaving logic to prevent button actions before edits are saved.
- */
-
-var SingletonListController = InventoryReportListController.extend({
-
-
- init: function (parent, model, renderer, params) {
- this.context = renderer.state.getContext();
- return this._super(...arguments);
- },
-
- willStart() {
- const sup = this._super(...arguments);
- const acl = this.getSession().user_has_group('stock.group_stock_manager').then(hasGroup => {
- if (hasGroup) {
- this.buttons_template = 'StockInventoryAdjustments.Buttons';
- }
- });
- return Promise.all([sup, acl]);
- },
-
- /**
- * @override
- */
- renderButtons: function ($node) {
- this._super(...arguments);
- if (this.context.inventory_report_mode) {
- this.$buttons.find('button.o_button_apply_all').hide();
- }
- this.$buttons.on('click', '.o_button_apply_all', this._onApplyAll.bind(this));
- },
-
- // -------------------------------------------------------------------------
- // Private
- // -------------------------------------------------------------------------
-
- /**
- * @override
- * @return {Promise} rejected when updating the list because we don't want
- * to select a cell that might not exist anymore.
- */
- _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 have tried to create a record which already exists. " +
- "The existing record has been modified instead.");
- this.displayNotification({ title: _t("This record already exists."), message: notification });
- this.reload();
- return Promise.reject();
- }
- else {
- return this._super.apply(this, arguments);
- }
- },
-
- // -------------------------------------------------------------------------
- // Handlers
- // -------------------------------------------------------------------------
-
- /**
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onButtonClicked: function (ev) {
- ev.stopPropagation();
- var self = this;
- return self.saveRecord(ev.data.record.id, {
- stayInEdit: true,
- }).then(function () {
- // we need to re-get the record to make sure we have changes made
- // by the basic model, such as the new res_id, if the record is
- // new.
- var record = self.model.get(ev.data.record.id);
- return self._callButtonAction(ev.data.attrs, record);
- }).then(function () {
- self._enableButtons();
- }).guardedCatch(this._enableButtons.bind(this));
- },
-
- _onApplyAll: async function () {
- const resIds = await this._domainToResIds(this.renderer.state.getDomain(), session.active_ids_limit);
- this.do_action('stock.action_stock_inventory_adjustement_name', {
- additional_context: {
- 'active_model': 'ir.ui.view',
- 'active_ids': resIds,
- },
- on_close: this.reload,
- });
- },
-});
-
-return SingletonListController;
-
-});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_renderer.js b/addons/stock/static/src/js/inventory_singleton_list_renderer.js
deleted file mode 100644
index 970f0ddfbec..00000000000
--- a/addons/stock/static/src/js/inventory_singleton_list_renderer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-odoo.define('stock.SingletonListRenderer', function (require) {
-"use strict";
-
-var ListRenderer = require('web.ListRenderer');
-
-/**
- * The purpose of this override is to disable line buttons when the row is edited.
- *
- */
-
-var SingletonListRenderer = ListRenderer.extend({
-
- _disableRecordSelectors: function () {
- this._super.apply(this, arguments);
- var row = this._getRow(this._getRecordID(this.currentRow))[0];
- var lineButtons = row.querySelectorAll('button[name=action_set_inventory_quantity]');
- lineButtons.forEach(elem => elem.setAttribute('disabled', true));
- },
- /**
- * @private
- */
- _enableRecordSelectors: function () {
- this._super.apply(this, arguments);
- var lineButtons = this.el.querySelectorAll('button.disabled');
- lineButtons.forEach(elem => elem.removeAttribute('disabled'));
- },
-});
-
-return SingletonListRenderer;
-
-});
diff --git a/addons/stock/static/src/js/inventory_singleton_list_view.js b/addons/stock/static/src/js/inventory_singleton_list_view.js
deleted file mode 100644
index 4c4855d8f96..00000000000
--- a/addons/stock/static/src/js/inventory_singleton_list_view.js
+++ /dev/null
@@ -1,20 +0,0 @@
-odoo.define('stock.SingletonListView', function (require) {
-'use strict';
-
-var InventoryReportListView = require('stock.InventoryReportListView');
-var SingletonListController = require('stock.SingletonListController');
-var SingletonListRenderer = require('stock.SingletonListRenderer');
-var viewRegistry = require('web.view_registry');
-
-var SingletonListView = InventoryReportListView.extend({
- config: _.extend({}, InventoryReportListView.prototype.config, {
- Controller: SingletonListController,
- Renderer: SingletonListRenderer,
- }),
-});
-
-viewRegistry.add('singleton_list', SingletonListView);
-
-return SingletonListView;
-
-});
diff --git a/addons/stock/static/src/views/list/inventory_report_list.scss b/addons/stock/static/src/views/list/inventory_report_list.scss
new file mode 100644
index 00000000000..368a76b160b
--- /dev/null
+++ b/addons/stock/static/src/views/list/inventory_report_list.scss
@@ -0,0 +1,13 @@
+
+.o_inventory_adjustments {
+
+ button[name='action_set_inventory_quantity'] {
+ pointer-events: auto;
+ }
+ .o_selected_row {
+ button[name='action_set_inventory_quantity'] {
+ pointer-events: none;
+ color: $text-muted;
+ }
+ }
+}
diff --git a/addons/stock/static/src/views/list/inventory_report_list.xml b/addons/stock/static/src/views/list/inventory_report_list.xml
new file mode 100644
index 00000000000..853f20d691b
--- /dev/null
+++ b/addons/stock/static/src/views/list/inventory_report_list.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/stock/static/src/views/list/inventory_report_list_controller.js b/addons/stock/static/src/views/list/inventory_report_list_controller.js
new file mode 100644
index 00000000000..e2ec1bfa697
--- /dev/null
+++ b/addons/stock/static/src/views/list/inventory_report_list_controller.js
@@ -0,0 +1,103 @@
+/** @odoo-module */
+
+import { ListController } from '@web/views/list/list_controller';
+import { useBus } from "@web/core/utils/hooks";
+import { session } from "@web/session";
+
+export class InventoryReportListController extends ListController {
+
+ setup() {
+ super.setup();
+ if (this.props.context.inventory_mode || this.props.context.inventory_report_mode) {
+ useBus(this.model, "record-updated",
+ this.recordUpdated
+ );
+ }
+ }
+
+ /**
+ * Handler called when the user clicked on the 'Valuation at Date' button.
+ * Opens wizard to display, at choice, the products inventory or a computed
+ * inventory at a given date.
+ */
+ async onClickInventoryAtDate() {
+ const context = {
+ active_model: this.props.resModel,
+ };
+ if (this.props.context.default_product_id) {
+ context.product_id = this.props.context.default_product_id;
+ } else if (this.props.context.product_tmpl_id) {
+ context.product_tmpl_id = this.props.context.product_tmpl_id;
+ }
+ this.actionService.doAction({
+ res_model: 'stock.quantity.history',
+ views: [[false, 'form']],
+ target: 'new',
+ type: 'ir.actions.act_window',
+ context,
+ });
+ }
+
+ /**
+ * Count 'resId' loaded records.
+ * Multiple records occurs when adding an already loaded-in-view record.
+ */
+ _countResIdRecords(level, resId) {
+ let count = 0;
+ if (level.groupByInfo && level.isGrouped) {
+ for (const i in level.groups) {
+ count += this._countResIdRecords(level.groups[i], resId);
+ }
+ }
+ else {
+ const records = level.groupByInfo ? level.list.records : level.records;
+ count += records.filter(record => record.resId == resId).length;
+ }
+ return count;
+ }
+
+ /**
+ * Handler called when a record has been created or updated.
+ * We need to detect when the user added to the list a quant which already exists
+ * (see stock.quant.create), either already loaded or not, to warn the user
+ * the quant was updated.
+ * This is done by checking :
+ * - the record id against the 'lastCreatedRecordId' on model
+ * - the create_date against the write_date (both are equal for newly created records).
+ */
+ async recordUpdated(event) {
+ const record = event.detail.record;
+ const justCreated = record.id == record.model.lastCreatedRecordId;
+ if (justCreated && record.data.create_date.diff(record.data.write_date) != 0) {
+ this.notificationService.add(
+ this.env._t("You tried to create a record that already exists. The existing record was modified instead."),
+ { title: this.env._t("This record already exists.") }
+ );
+ if (this._countResIdRecords(record.model.root, record.resId) > 1) {
+ /* more than 1 'resId' record loaded in view (user added an already loaded record) :
+ * - both have been updated
+ * - remove the current record (the added one)
+ */
+ this.model.root.removeRecord(record);
+ }
+ }
+ }
+
+ /**
+ * Handler called when the user clicked on the 'Apply all' button.
+ */
+ async onClickApplyAll() {
+ const activeIds = await this.model.orm.search(this.props.resModel, this.props.domain, {
+ limit: session.active_ids_limit,
+ context: this.props.context,
+ });
+ return this.actionService.doAction('stock.action_stock_inventory_adjustement_name', {
+ additionalContext: {
+ active_ids: activeIds,
+ },
+ onClose: () => {
+ this.model.load();
+ },
+ });
+ }
+}
diff --git a/addons/stock/static/src/views/list/inventory_report_list_model.js b/addons/stock/static/src/views/list/inventory_report_list_model.js
new file mode 100644
index 00000000000..b3b41ad0f0f
--- /dev/null
+++ b/addons/stock/static/src/views/list/inventory_report_list_model.js
@@ -0,0 +1,30 @@
+/** @odoo-module */
+
+import { RelationalModel, DynamicRecordList } from "@web/views/relational_model";
+
+export class InventoryReportListModel extends RelationalModel {
+
+ /**
+ * Override
+ */
+ setup(params, { action, dialog, notification, rpc, user, view, company }) {
+ // model has not created any record yet
+ this.lastCreatedRecordId = "";
+ return super.setup(...arguments);
+ }
+}
+
+export class InventoryReportListDynamicRecordList extends DynamicRecordList {
+
+ /**
+ * Override
+ */
+ async createRecord(params = {}, atFirstPosition = false) {
+ const record = await super.createRecord(...arguments);
+ // keep created record id on model
+ record.model.lastCreatedRecordId = record.id;
+ return record;
+ }
+}
+
+InventoryReportListModel.DynamicRecordList = InventoryReportListDynamicRecordList;
diff --git a/addons/stock/static/src/views/list/inventory_report_list_view.js b/addons/stock/static/src/views/list/inventory_report_list_view.js
new file mode 100644
index 00000000000..bce20f97ee8
--- /dev/null
+++ b/addons/stock/static/src/views/list/inventory_report_list_view.js
@@ -0,0 +1,15 @@
+/** @odoo-module */
+
+import { listView } from "@web/views/list/list_view";
+import { InventoryReportListModel } from "./inventory_report_list_model";
+import { InventoryReportListController } from "./inventory_report_list_controller";
+import { registry } from "@web/core/registry";
+
+export const InventoryReportListView = {
+ ...listView,
+ Model: InventoryReportListModel,
+ Controller: InventoryReportListController,
+ buttonTemplate: 'InventoryReport.Buttons',
+};
+
+registry.category("views").add('inventory_report_list', InventoryReportListView);
diff --git a/addons/stock/static/src/xml/inventory_adjustments.xml b/addons/stock/static/src/xml/inventory_adjustments.xml
deleted file mode 100644
index b423d7e70cf..00000000000
--- a/addons/stock/static/src/xml/inventory_adjustments.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/stock/static/src/xml/inventory_report.xml b/addons/stock/static/src/xml/inventory_report.xml
deleted file mode 100644
index 5b4484eeb1f..00000000000
--- a/addons/stock/static/src/xml/inventory_report.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/stock/static/tests/inventory_report_list_tests.js b/addons/stock/static/tests/inventory_report_list_tests.js
new file mode 100644
index 00000000000..138b7b59570
--- /dev/null
+++ b/addons/stock/static/tests/inventory_report_list_tests.js
@@ -0,0 +1,200 @@
+/** @odoo-module **/
+
+import { getFixture, click, editInput } from "@web/../tests/helpers/utils";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+
+const { DateTime } = luxon;
+
+let target, serverData;
+
+const arch =
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ '';
+
+const setup_date = DateTime.fromISO('2022-01-03T08:03:44+00:00').toSQL();
+
+function mockRPC(route, args) {
+ if (route === '/web/dataset/call_kw/person/create') {
+ // simulate 'stock.quant' create function which can return existing record
+ args.args[0].create_date = DateTime.now().toSQL();
+ args.args[0].write_date = args.args[0].create_date;
+ var name = args.args[0].name;
+ var age = args.args[0].age;
+ var job = args.args[0].job;
+ for (var d of serverData.models.person.records) {
+ if (d.name === name) {
+ d.age = age;
+ d.job = job;
+ d.write_date = args.args[0].write_date;
+ return Promise.resolve(d.id);
+ }
+ }
+ }
+}
+
+QUnit.module(
+ "Views",
+ {
+ beforeEach() {
+ target = getFixture();
+ serverData = {
+ models: {
+ person: {
+ fields: {
+ name: {string: "Name", type: "char"},
+ age: {string: "Age", type: "integer"},
+ job: {string: "Profession", type: "char"},
+ // standard fields
+ create_date: {string: "Created on", type: "datetime" },
+ write_date: {string: "Last Updated on", type: "datetime" },
+ },
+ records: [
+ {id: 1, name: 'Daniel Fortesque', age: 32, job: 'Soldier', create_date: setup_date, write_date: setup_date},
+ {id: 2, name: 'Samuel Oak', age: 64, job: 'Professor', create_date: setup_date, write_date: setup_date},
+ {id: 3, name: 'Leto II Atreides', age: 128, job: 'Emperor', create_date: setup_date, write_date: setup_date},
+ ]
+ },
+ },
+ };
+ setupViewRegistries();
+ },
+ },
+ function () {
+
+ QUnit.module("InventoryReportListView");
+
+ QUnit.test('Create new record correctly', async function (assert) {
+
+ assert.expect(2);
+
+ await makeView({
+ type: 'list',
+ resModel: 'person',
+ serverData,
+ arch: arch,
+ mockRPC: mockRPC,
+ context: {
+ inventory_mode: true,
+ },
+ });
+
+ // Check we have initially 3 records
+ assert.containsN(target, '.o_data_row', 3, "should have 3 records");
+
+ // Create a new line...
+ await click(target.querySelector(".o_list_button_add"));
+ await editInput(target, "[name=name] input", 'Bilou');
+ await editInput(target, "[name=age] input", '24');
+ await click(target.querySelector(".o_list_button_save"));
+
+ // Check new record is in the list
+ assert.containsN(target, '.o_data_row', 4, "should now have 4 records");
+ });
+
+ QUnit.test('Don\'t duplicate record', async function (assert) {
+
+ assert.expect(3);
+
+ await makeView({
+ type: 'list',
+ resModel: 'person',
+ serverData,
+ arch: arch,
+ mockRPC: mockRPC,
+ context: {
+ inventory_mode: true,
+ },
+ });
+
+ // Check we have initially 3 records
+ assert.containsN(target, '.o_data_row', 3, "should have 3 records");
+
+ // Create a new line for an existing record...
+ let name = serverData.models.person.records[2].name, age = '72';
+ await click(target.querySelector(".o_list_button_add"));
+ await editInput(target, "[name=name] input", name);
+ await editInput(target, "[name=age] input", age);
+ await click(target.querySelector(".o_list_button_save"));
+
+ // Check we still have 3 records...
+ assert.containsN(target, '.o_data_row', 3, "should still have 3 records");
+ // ... and verify update has occurred.
+ var nameField =$(target).find('td[data-tooltip="' + name + '"]');
+ var ageField = nameField.parent().find('.o_list_number');
+ assert.strictEqual(ageField.text(), age, "The age field must be updated");
+ });
+
+ QUnit.test('Work in grouped list', async function (assert) {
+
+ assert.expect(6);
+
+ await makeView({
+ type: "list",
+ resModel: 'person',
+ serverData,
+ arch: arch,
+ mockRPC: mockRPC,
+ context: {
+ inventory_mode: true,
+ },
+ groupBy: ['job'], // Groups are Emperor, Professor, Soldier
+ });
+
+ // Open 'Professor' group
+ await click(target.querySelectorAll(".o_group_header")[1]);
+
+ // Check we have only 1 record...
+ assert.containsN(target, '.o_data_row', 1, "should have 1 record");
+
+ // Create a new record...
+ let name = 'Del Tutorial', age = "32";
+ await click(target, ".o_group_field_row_add a");
+ await editInput(target, "[name=name] input", name);
+ await editInput(target, "[name=age] input", age);
+ await click(target.querySelector(".o_list_button_save"));
+ // Check we have 2 records...
+ assert.containsN(target, '.o_data_row', 2, "should have 2 records");
+
+ // Create an existing record...
+ name = serverData.models.person.records[1].name;
+ age = "55";
+ await click(target, ".o_group_field_row_add a");
+ await editInput(target, "[name=name] input", name);
+ await editInput(target, "[name=age] input", age);
+ await click(target.querySelector(".o_list_button_save"));
+ // Check we still have 2 records...
+ assert.containsN(target, '.o_data_row', 2, "should still have 2 records");
+
+ // Create an existing but not displayed record...
+ name = serverData.models.person.records[0].name;
+ await click(target, ".o_group_field_row_add a");
+ await editInput(target, "[name=name] input", name);
+ await editInput(target, "[name=age] input", age);
+ await editInput(target, "[name=job] input", "Soldier"); // let it in its original group
+ await click(target.querySelector(".o_list_button_save"));
+ // Check we have 3 records...
+ assert.containsN(target, '.o_data_row', 3, "should have 3 records");
+
+ // Opens 'Soldier' group
+ await click(target.querySelectorAll(".o_group_header")[2]);
+ let rows = target.querySelectorAll(".o_data_row");
+
+ // Check 'original' record has been updated...
+ // : Daniel Fortesque is in record 0 for group Soldier and in record 3 for group Professor
+ assert.strictEqual(rows[0].querySelector('[name="age"]').textContent, age, "age of the record must be updated");
+
+ // Edit the freshly created record...
+ await click(rows[3].querySelector(".o_field_cell"));
+ await editInput(target, "[name=age] input", "66");
+ await click(target, ".o_list_view");
+
+ // Check both records have been updated...
+ assert.strictEqual(rows[0].querySelector('[name="age"]').textContent, rows[3].querySelector('[name="age"]').textContent, "age of the record must be updated");
+ });
+ }
+);
diff --git a/addons/stock/static/tests/singleton_list_tests.js b/addons/stock/static/tests/singleton_list_tests.js
deleted file mode 100644
index 320f0779dfd..00000000000
--- a/addons/stock/static/tests/singleton_list_tests.js
+++ /dev/null
@@ -1,305 +0,0 @@
-odoo.define('web.singleton_list_tests', function (require) {
-"use strict";
-
-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');
-
- // Checks we have still only 3 records...
- assert.containsN(list, '.o_data_row', 3, "should still have 3 records");
- // ... and verify modification was occurred.
- 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('Don\'t raise error when trying to create duplicate line', async function (assert) {
- assert.expect(3);
- /* In some condition, a list editable with the `singletonlist` js_class
- can try to select a record at a line who isn't the same place anymore.
- In this case, the list can try to find the id of an undefined record.
- This test just insures we don't raise a traceback in this case.
- */
- var list = await createView({
- View: SingletonListView,
- model: 'person',
- data: {
- person: {
- fields: {
- name: {string: "Name", type: "char"},
- age: {string: "Age", type: "integer"},
- },
- records: [
- {id: 1, name: 'Bobby B. Bop', age: 18},
- ]
- }
- },
- arch: ''+
- ''+
- ''+
- '',
- mockRPC: this.mockRPC,
- });
- // Checks we have initially 1 record
- assert.containsN(list, '.o_data_row', 1, "should have 1 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 = 'Bobby B. Bop';
- await testUtils.fields.editInput($input, name);
- await testUtils.fields.triggerKeydown($input, 'tab');
-
- $input = $('.o_selected_row input[name=age]');
- var age = '22';
- await testUtils.fields.editInput($input, age);
- // This operation causes list'll try to select undefined record.
- await testUtils.fields.triggerKeydown($input, 'enter');
-
- // Checks we have still only 1 record...
- assert.containsN(list, '.o_data_row', 1, "should now have 1 records");
- // ... and verify modification was occurred.
- 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 7968323cadc..c84ec86681f 100644
--- a/addons/stock/views/stock_quant_views.xml
+++ b/addons/stock/views/stock_quant_views.xml
@@ -102,8 +102,10 @@
+
+
@@ -343,12 +345,14 @@
stock.quant
-
+
+
+
diff --git a/addons/stock_account/static/src/xml/inventory_report.xml b/addons/stock_account/static/src/xml/inventory_report.xml
index f1679721249..e4c6cce3f1e 100644
--- a/addons/stock_account/static/src/xml/inventory_report.xml
+++ b/addons/stock_account/static/src/xml/inventory_report.xml
@@ -1,9 +1,9 @@
-
+
-
+
Valuation at Date