[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:
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user