[REF] stock: convert inventory_report_list to wowl
closes odoo/odoo#98436 Task: 2885757 Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="InventoryReport.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="(//div/*)[last()]" position="after">
|
||||
<button t-if="props.resModel == 'stock.quant' and !props.context.inventory_mode and !props.context.no_at_date"
|
||||
type="button" class="btn btn-primary o_button_at_date me-2" t-on-click="onClickInventoryAtDate">
|
||||
Inventory at Date
|
||||
</button>
|
||||
<button t-if="props.context.inventory_mode and !props.context.inventory_report_mode"
|
||||
class="btn btn-secondary" type="button" t-on-click="onClickApplyAll">
|
||||
Apply All
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="ListView.buttons" t-name="StockInventoryAdjustments.Buttons">
|
||||
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||
<button class="btn btn-secondary o_button_apply_all" type="button">
|
||||
Apply All
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="ListView.buttons" t-name="StockInventoryReport.Buttons">
|
||||
<t t-jquery="button.o_list_button_discard" t-operation="after">
|
||||
<button class="btn btn-primary o_button_at_date me-2" type="button">
|
||||
<t t-if="widget.modelName == 'stock.quant'">
|
||||
Inventory at Date
|
||||
</t>
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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 =
|
||||
'<tree editable="top" js_class="inventory_report_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'<field name="job"/>'+
|
||||
'<field name="create_date" invisible="1"/>'+
|
||||
'<field name="write_date" invisible="1"/>'+
|
||||
'</tree>';
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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: '<tree editable="top" js_class="singleton_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'</tree>',
|
||||
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: '<tree editable="top" js_class="singleton_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'</tree>',
|
||||
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: '<tree editable="top" js_class="singleton_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'</tree>',
|
||||
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: '<tree editable="top" js_class="singleton_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'</tree>',
|
||||
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: '<tree editable="top" js_class="singleton_list">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="age"/>'+
|
||||
'<field name="job"/>'+
|
||||
'</tree>',
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -102,8 +102,10 @@
|
||||
<field eval="5" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom"
|
||||
create="1" edit="1" js_class="singleton_list"
|
||||
create="1" edit="1" js_class="inventory_report_list"
|
||||
sample="1">
|
||||
<field name="create_date" invisible="1"/>
|
||||
<field name="write_date" invisible="1"/>
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
@@ -343,12 +345,14 @@
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" decoration-warning='is_outdated' editable="bottom" create="1" edit="1" import="1" js_class="singleton_list" sample="1">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" decoration-warning='is_outdated' editable="bottom" create="1" edit="1" import="1" js_class="inventory_report_list" class="o_inventory_adjustments" sample="1">
|
||||
<header>
|
||||
<button name="stock.action_stock_inventory_adjustement_name" groups="stock.group_stock_manager" type="action" string="Apply"/>
|
||||
<button name="action_reset" type="object" string="Clear"/>
|
||||
<button name="stock.action_stock_request_count" groups="stock.group_stock_manager" type="action" string="Request a Count"/>
|
||||
</header>
|
||||
<field name="create_date" invisible="1"/>
|
||||
<field name="write_date" invisible="1"/>
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="is_outdated" invisible="1"/>
|
||||
<field name="sn_duplicated" invisible="1"/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-extend="StockInventoryReport.Buttons">
|
||||
<t t-extend="InventoryReport.Buttons" owl="1">
|
||||
<t t-jquery="button.o_button_at_date" t-operation="append">
|
||||
<t t-if="widget.modelName == 'stock.valuation.layer'">
|
||||
<t t-if="props.resModel == 'stock.valuation.layer'">
|
||||
Valuation at Date
|
||||
</t>
|
||||
</t>
|
||||
|
||||
Reference in New Issue
Block a user