[REM] barcodes: simplify barcode_handler widget

The previous commit removes a good part of the barcode form view. The
rest of the code was only used by the barcode_handler widget. Since it
is now much simpler, we can just implement the behaviour: simply set the
value to the barcode, so onchanges can be properly called.

closes odoo/odoo#95892

Signed-off-by: Adrien Dieudonné (adr) <adr@odoo.com>
This commit is contained in:
Géry Debongnie
2022-07-19 11:50:32 +02:00
parent ce6f13c3cd
commit 603e5a214f
4 changed files with 16 additions and 403 deletions
@@ -56,8 +56,8 @@ const COMMANDS = {
};
export const barcodeGenericHandlers = {
dependencies: ["ui", "barcode"],
start(env, { ui, barcode }) {
dependencies: ["ui", "barcode", "notification"],
start(env, { ui, barcode, notification }) {
barcode.bus.addEventListener("barcode_scanned", (ev) => {
const barcode = ev.detail.barcode;
@@ -79,7 +79,10 @@ export const barcodeGenericHandlers = {
if (fn) {
fn();
} else {
console.warn(`Barcode '${barcode}' is not a valid command`);
notification.add(env._t("Barcode: ") + `'${barcode}'`, {
title: env._t("Unknown barcode command"),
type: "danger"
});
}
}
});
+9 -13
View File
@@ -4,6 +4,7 @@ odoo.define('barcodes.field', function(require) {
var AbstractField = require('web.AbstractField');
var basicFields = require('web.basic_fields');
var fieldRegistry = require('web.field_registry');
var core = require('web.core');
// Field in which the user can both type normally and scan barcodes
@@ -37,29 +38,24 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({
}
});
// Field to use scan barcodes
var FormViewBarcodeHandler = AbstractField.extend({
/**
* @override
*/
init: function() {
this._super.apply(this, arguments);
this.trigger_up('activeBarcode', {
name: this.name,
commands: {
barcode: '_barcodeAddX2MQuantity',
}
});
core.bus.on('barcode_scanned', this, this._barcodeScanned);
},
destroy: function () {
core.bus.off('barcode_scanned', this, this._barcodeScanned);
this._super();
},
_barcodeScanned(barcode) {
this._setValue(barcode);
},
});
fieldRegistry.add('field_float_scannable', FieldFloatScannable);
fieldRegistry.add('barcode_handler', FormViewBarcodeHandler);
return {
FieldFloatScannable: FieldFloatScannable,
FormViewBarcodeHandler: FormViewBarcodeHandler,
};
});
@@ -1,255 +0,0 @@
odoo.define('barcodes.FormView', function (require) {
"use strict";
var concurrency = require('web.concurrency');
var core = require('web.core');
var FormController = require('web.FormController');
var _t = core._t;
const reservedBarcodePrefixes = ['O-CMD'];
FormController.include({
custom_events: _.extend({}, FormController.prototype.custom_events, {
activeBarcode: '_barcodeActivated',
}),
/**
* add default barcode commands for from view
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.activeBarcode = {
form_view: {
commands: {
},
},
};
this.barcodeMutex = new concurrency.Mutex();
this._barcodeStartListening();
},
/**
* @override
*/
destroy: function () {
this._barcodeStopListening();
this._super();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Promise}
*/
_barcodeAddX2MQuantity: function (barcode, activeBarcode) {
if (this.mode === 'readonly') {
this.displayNotification({ message: _t('Enable edit mode to modify this document'), type: 'danger' });
return Promise.reject();
}
var record = this.model.get(this.handle);
var candidate = this._getBarCodeRecord(record, barcode, activeBarcode);
if (candidate) {
return this._barcodeSelectedCandidate(candidate, record, barcode, activeBarcode);
} else {
return this._barcodeWithoutCandidate(record, barcode, activeBarcode);
}
},
/**
* Returns true iff the given barcode matches the given record (candidate).
*
* @private
* @param {Object} candidate: record in the x2m
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {boolean}
*/
_barcodeRecordFilter: function (candidate, barcode, activeBarcode) {
return candidate.data.product_barcode === barcode;
},
/**
* @private
* @param {Object} candidate: record in the x2m
* @param {Object} current record
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Promise}
*/
_barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode, quantity) {
var changes = {};
var candidateChanges = {};
candidateChanges[activeBarcode.quantity] = quantity ? quantity : candidate.data[activeBarcode.quantity] + 1;
changes[activeBarcode.fieldName] = {
operation: 'UPDATE',
id: candidate.id,
data: candidateChanges,
};
return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
*/
_barcodeStartListening: function () {
core.bus.on('barcode_scanned', this, this._barcodeScanned);
},
/**
* @private
*/
_barcodeStopListening: function () {
core.bus.off('barcode_scanned', this, this._barcodeScanned);
},
/**
* @private
* @param {Object} current record
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Promise}
*/
_barcodeWithoutCandidate: function (record, barcode, activeBarcode) {
var changes = {};
changes[activeBarcode.name] = barcode;
return this.model.notifyChanges(record.id, changes);
},
/**
* @private
* @param {Object} current record
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Object|undefined}
*/
_getBarCodeRecord: function (record, barcode, activeBarcode) {
var self = this;
if (!activeBarcode.fieldName || !record.data[activeBarcode.fieldName]) {
return;
}
return _.find(record.data[activeBarcode.fieldName].data, function (record) {
return self._barcodeRecordFilter(record, barcode, activeBarcode);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* The barcode is activate when at least one widget trigger_up 'activeBarcode' event
* with the widget option
*
* @param {OdooEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
* @param {boolean} [event.data.notifyChange] optional for x2many sub field
* do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
* or change 'barcode' for all other received barcodes
* (e.g.: 'O-CMD.MAIN-MENU': function ..., barcode: function () {...})
*/
_barcodeActivated: function (event) {
event.stopPropagation();
var name = event.data.name;
this.activeBarcode[name] = {
name: name,
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
this.activeBarcode[name].candidate : null,
};
// we want to disable autofocus when activating the barcode to avoid
// putting the scanned value in the focused field
this.disableAutofocus = true;
},
/**
* @private
* @param {string|function} method defined by the commands options
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Promise}
*/
_barcodeActiveScanned: function (method, barcode, activeBarcode) {
var self = this;
var methodDef;
var def = new Promise(function (resolve, reject) {
if (typeof method === 'string') {
methodDef = self[method](barcode, activeBarcode);
} else {
methodDef = method.call(self, barcode, activeBarcode);
}
methodDef
.then(function () {
var record = self.model.get(self.handle);
var candidate = self._getBarCodeRecord(record, barcode, activeBarcode);
activeBarcode.candidate = candidate;
})
.then(resolve, resolve);
});
return def;
},
/**
* Method called when a user scan a barcode, call each method in function of the
* widget options then update the renderer
*
* @private
* @param {string} barcode sent by the scanner (string generate from keypress series)
* @param {DOM Object} target
* @returns {Promise}
*/
_barcodeScanned: function (barcode, target) {
var self = this;
return this.barcodeMutex.exec(function () {
var prefixed = _.any(reservedBarcodePrefixes,
function (reserved) {return barcode.indexOf(reserved) === 0;});
var hasCommand = false;
var defs = [];
if (! $.contains(target, self.el)) {
return;
}
for (var k in self.activeBarcode) {
var activeBarcode = self.activeBarcode[k];
// Handle the case where there are several barcode widgets on the same page. Since the
// event is global on the page, all barcode widgets will be triggered. However, we only
// want to keep the event on the target widget.
var methods = self.activeBarcode[k].commands;
var method = prefixed ? methods[barcode] : methods.barcode;
if (method) {
if (prefixed) {
hasCommand = true;
}
defs.push(self._barcodeActiveScanned(method, barcode, activeBarcode));
}
}
if (prefixed && !hasCommand) {
self.displayNotification({ title: _t('Undefined barcode command'), message: barcode, type: 'danger' });
}
return self.alive(Promise.all(defs)).then(function () {
if (!prefixed) {
// remember the barcode scanned for the quantity listener
self.current_barcode = barcode;
// redraw the view if we scanned a real barcode (required if
// we manually apply the change in JS, e.g. incrementing the
// quantity)
self.update({}, {reload: false});
}
});
});
},
});
});
+1 -132
View File
@@ -8,8 +8,6 @@ const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
const { registry } = require("@web/core/registry");
const { mockTimeout } = require("@web/../tests/helpers/utils");
var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
@@ -178,7 +176,6 @@ QUnit.test('pager buttons', async function (assert) {
var form = await createView({
View: FormView,
debug: true,
model: 'product',
data: this.data,
arch: '<form><field name="display_name"/></form>',
@@ -215,7 +212,7 @@ QUnit.test('pager buttons', async function (assert) {
});
QUnit.test('do no update form twice after a command barcode scanned', async function (assert) {
assert.expect(7);
assert.expect(5);
testUtils.mock.patch(FormController, {
update: function () {
@@ -254,12 +251,6 @@ QUnit.test('do no update form twice after a command barcode scanned', async func
// update shouldn't be called afterwards
assert.verifySteps(['update', 'read']);
simulateBarCode(['5','4','3','9','8','2','6','7','1','2','5','2','Enter']);
await testUtils.nextTick();
// a real barcode has been scanned -> an update should be requested (with
// option reload='false', so it isn't followed by a read)
assert.verifySteps(['update']);
form.destroy();
testUtils.mock.unpatch(FormController);
});
@@ -323,126 +314,4 @@ QUnit.test('widget field_float_scannable', async function (assert) {
core.bus.off('barcode_scanned', null, _onBarcodeScanned)
});
QUnit.test('widget barcode_handler', async function (assert) {
assert.expect(4);
this.data.product.fields.barcode_scanned = {string : "Scanned barcode", type: "char"};
this.data.product.onchanges = {
barcode_scanned: function (obj) {
// simulate an onchange that increment the int_field value
// at each barcode scanned
obj.int_field = obj.int_field + 1;
},
};
var form = await createView({
View: FormView,
model: 'product',
data: this.data,
arch: '<form>' +
'<field name="display_name"/>' +
'<field name="int_field"/>' +
'<field name="barcode_scanned" widget="barcode_handler"/>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.step('onchange');
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '0',
"initial value should be correct");
simulateBarCode(['5','4','3','9','8','2','6','7','1','2','5','2','Enter']);
await testUtils.nextTick();
assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '1',
"value should have been incremented");
assert.verifySteps(['onchange'], "an onchange should have been done");
form.destroy();
});
QUnit.test('specification of widget barcode_handler', async function (assert) {
assert.expect(5);
// Define a specific barcode_handler widget for this test case
var TestBarcodeHandler = AbstractField.extend({
init: function () {
this._super.apply(this, arguments);
this.trigger_up('activeBarcode', {
name: 'test',
fieldName: 'line_ids',
quantity: 'quantity',
commands: {
barcode: '_barcodeAddX2MQuantity',
}
});
},
});
fieldRegistry.add('test_barcode_handler', TestBarcodeHandler);
var form = await createView({
View: FormView,
model: 'order',
data: this.data,
arch: '<form>' +
'<field name="_barcode_scanned" widget="test_barcode_handler"/>' +
'<field name="line_ids">' +
'<tree>' +
'<field name="product_id"/>' +
'<field name="product_barcode" invisible="1"/>' +
'<field name="quantity"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.notOK(true, "should not do any onchange RPC");
}
if (args.method === 'write') {
assert.deepEqual(args.args[1].line_ids, [
[1, 1, {quantity: 2}], [1, 2, {quantity: 1}],
], "should have generated the correct commands");
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.containsN(form, '.o_data_row', 2,
"one2many should contain 2 rows");
// scan twice product 1
simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']);
await testUtils.nextTick();
assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '1',
"quantity of line one should have been incremented");
simulateBarCode(['1','2','3','4','5','6','7','8','9','0','Enter']);
await testUtils.nextTick();
assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '2',
"quantity of line one should have been incremented");
// scan once product 2
simulateBarCode(['0','9','8','7','6','5','4','3','2','1','Enter']);
await testUtils.nextTick();
assert.strictEqual(form.$('.o_data_row:nth(1) .o_data_cell:nth(1)').text(), '1',
"quantity of line one should have been incremented");
await testUtils.form.clickSave(form);
form.destroy();
delete fieldRegistry.map.test_barcode_handler;
});
});