From 8cdcc933ca12276a5444f329570798247f6adff1 Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Wed, 7 Sep 2022 11:42:06 +0000 Subject: [PATCH] [IMP] barcodes{,_gs1_nomenclature}: interpret GS character Let's say the company use a GS1 nomenclature and a user scans such a barcode: `(01)11111111111113(10)xyz(30)40` It means a product P: - that has the barcode value '11111111111113' - for the lot 'xyz' - with a quantity equal to 40 The GS1 nomenclature explains that a lot consists of a prefix `10` and of up to 20 alphanumeric characters. In the above example, we could believe that the lot is actually `xyz3040`. To prevent this kind of issue, the GS1 standards use a special character: the Group Separator (its hex value is `\x1d` and does not have any visual representation). So, when reading the barcode of the above value, the barcode reader will detect the GS between `z` and `3` (so we know that this is the end of the lot name). Because GS has no visual representation, the way the GS character is given by the scanner to the device depends on the scanner itself. For instance, some barcode readers try to encode GS thanks to its unicode input (as used on a Windows device): it enters Alt+0+2+9 (i.e., the unicode value of GS). This is an issue since we filter out all keydown events with the `Alt` key pressed. Also, our way to interpret the barcode value depends on the device: on a desktop device, we listen the events. As explained above, this could be an issue (for instance with Alt+029). On a mobile device, we let the barcode reader write in an hidden input and we then extract the written value. There is also an issue here: the GS is not present in the extracted string. For these reasons, some changes are need. The idea is to let the user lists the ways his barcode readers will encode the GS character (the scanners often give the possibility to set a value manually.). There is already a field for this: https://github.com/odoo/odoo/blob/320025ee630acadd4a5cbd32f425cddee4f81c1e/addons/barcodes_gs1_nomenclature/models/barcode_nomenclature.py#L18-L20 However, this is not correctly working. Suppose we now stop to filter out the `Alt` keys: in the above example, the scanned value becomes `011111111111111310xyzAlt0293040`(as you can see, there is an `Alt029` at the end of the lot). Suppose also that we defined `gs1_separator_fnc1` with `Alt029`. When parsing the barcode value, we have: https://github.com/odoo/odoo/blob/1c0dec6d5813fa81a0fec8668ee13d8036cde5c5/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js#L91-L98 So, when we try to extract the lot name, the values will be: - barcode: `10xyzAlt0293040` - regex: `^(10)([!\"%-/0-9:-?A-Z_a-z]{0,20})(?:(Alt029))?` And here is the issue: `Alt029` will be caught by the second capturing group of the regex (instead of the third one), so it will be considered as part of the lot name, we still have an issue => considering the `Alt`/`Control` keys and defining some values in `gs1_separator_fnc1` are not enough, we need to parse twice: - The first parsing is used to convert all occurrences of `gs1_separator_fnc1` into the hex `\x1d`. Then, it is also used to remove all useless `Alt`/`Control`/`Shift`. - The second parsing is the already-existing one and this parser is already able to catch all `\x1d` as group separators: https://github.com/odoo/odoo/blob/1c0dec6d5813fa81a0fec8668ee13d8036cde5c5/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js#L90 For the mobile device, it means that the users will have to define a special character that stands for GS (e.g., `#`), so we will retrieve it in the input. OPW-2930873 closes odoo/odoo#99730 Signed-off-by: William Henrotin (whe) --- addons/barcodes/static/src/barcode_service.js | 15 +++++++--- .../barcodes_gs1_nomenclature/__manifest__.py | 1 + .../models/__init__.py | 1 + .../models/barcode_nomenclature.py | 4 +-- .../models/ir_http.py | 16 ++++++++++ .../static/src/js/barcode_parser.js | 8 ++--- .../static/src/js/barcode_service.js | 18 ++++++++++++ .../src/js/tests/barcode_parser_tests.js | 29 ------------------- 8 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 addons/barcodes_gs1_nomenclature/models/ir_http.py create mode 100644 addons/barcodes_gs1_nomenclature/static/src/js/barcode_service.js diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js index f449c01a53e..0c1a2fb6e2b 100644 --- a/addons/barcodes/static/src/barcode_service.js +++ b/addons/barcodes/static/src/barcode_service.js @@ -28,6 +28,10 @@ export const barcodeService = { // this is done here to make it easily mockable in mobile tests isMobileChrome: isMobileOS() && isBrowserChrome(), + cleanBarcode: function(barcode) { + return barcode.replace(/Alt|Shift|Control/g, ''); + }, + start() { const bus = new EventBus(); let timeout = null; @@ -47,7 +51,8 @@ export const barcodeService = { * check if we have a barcode, and trigger appropriate events */ function checkBarcode() { - const str = barcodeInput ? barcodeInput.value : bufferedBarcode; + let str = barcodeInput ? barcodeInput.value : bufferedBarcode; + str = barcodeService.cleanBarcode(str); if (str.length >= 3) { handleBarcode(str, currentTarget); } @@ -66,9 +71,11 @@ export const barcodeService = { return; } // Ignore 'Shift', 'Escape', 'Backspace', 'Insert', 'Delete', 'Home', 'End', Arrow*, F*, Page*, ... - // ctrl, meta and alt are often used for UX purpose (like shortcuts) - // Note: shiftKey is not ignored because it can be used by some barcode scanner for digits. - const isSpecialKey = ev.key.length > 1 || ev.ctrlKey || ev.metaKey || ev.altKey; + // meta is often used for UX purpose (like shortcuts) + // Notes: + // - shiftKey is not ignored because it can be used by some barcode scanner for digits. + // - altKey/ctrlKey are not ignored because it can be used in some barcodes (e.g. GS1 separator) + const isSpecialKey = !['Control', 'Alt'].includes(ev.key) && (ev.key.length > 1 || ev.metaKey); const isEndCharacter = ev.key.match(/(Enter|Tab)/); // Don't catch non-printable keys except 'enter' and 'tab' diff --git a/addons/barcodes_gs1_nomenclature/__manifest__.py b/addons/barcodes_gs1_nomenclature/__manifest__.py index 288b0111dca..671103a611a 100644 --- a/addons/barcodes_gs1_nomenclature/__manifest__.py +++ b/addons/barcodes_gs1_nomenclature/__manifest__.py @@ -14,6 +14,7 @@ 'assets': { 'web.assets_backend': [ 'barcodes_gs1_nomenclature/static/src/js/barcode_parser.js', + 'barcodes_gs1_nomenclature/static/src/js/barcode_service.js', ], 'web.qunit_suite_tests': [ 'barcodes_gs1_nomenclature/static/src/js/tests/**/*', diff --git a/addons/barcodes_gs1_nomenclature/models/__init__.py b/addons/barcodes_gs1_nomenclature/models/__init__.py index 01218f2351b..d979a2928fc 100644 --- a/addons/barcodes_gs1_nomenclature/models/__init__.py +++ b/addons/barcodes_gs1_nomenclature/models/__init__.py @@ -1,2 +1,3 @@ from . import barcode_nomenclature from . import barcode_rule +from . import ir_http diff --git a/addons/barcodes_gs1_nomenclature/models/barcode_nomenclature.py b/addons/barcodes_gs1_nomenclature/models/barcode_nomenclature.py index b9a3df7d7ef..3ed7cec3ba1 100644 --- a/addons/barcodes_gs1_nomenclature/models/barcode_nomenclature.py +++ b/addons/barcodes_gs1_nomenclature/models/barcode_nomenclature.py @@ -16,8 +16,8 @@ class BarcodeNomenclature(models.Model): string="Is GS1 Nomenclature", help="This Nomenclature use the GS1 specification, only GS1-128 encoding rules is accepted is this kind of nomenclature.") gs1_separator_fnc1 = fields.Char( - string="FNC1 Separator", trim=False, - help="Alternative regex delimiter for the FNC1 (by default, if not set, it is ASCII 29 char). The separator must not match the begin/end of any related rules pattern.") + string="FNC1 Separator", trim=False, default=r'(Alt029|#|\x1D)', + help="Alternative regex delimiter for the FNC1. The separator must not match the begin/end of any related rules pattern.") @api.constrains('gs1_separator_fnc1') def _check_pattern(self): diff --git a/addons/barcodes_gs1_nomenclature/models/ir_http.py b/addons/barcodes_gs1_nomenclature/models/ir_http.py new file mode 100644 index 00000000000..58b95bfefdc --- /dev/null +++ b/addons/barcodes_gs1_nomenclature/models/ir_http.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class IrHttp(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + res = super().session_info() + nomenclature = self.env.company.nomenclature_id + if not nomenclature.is_gs1_nomenclature: + return res + res['gs1_group_separator_encodings'] = nomenclature.gs1_separator_fnc1 + return res diff --git a/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js b/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js index 1de5b782326..f17e14e5881 100644 --- a/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js +++ b/addons/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js @@ -88,9 +88,6 @@ BarcodeParser.include({ const results = []; const rules = this.nomenclature.rules.filter(rule => rule.encoding === 'gs1-128'); let separatorReg = FNC1_CHAR + "?"; - if (this.nomenclature.gs1_separator_fnc1 && this.nomenclature.gs1_separator_fnc1.trim()){ - separatorReg = `(?:${this.nomenclature.gs1_separator_fnc1})?`; - } while (barcode.length > 0) { const barcodeLength = barcode.length; @@ -151,5 +148,8 @@ BarcodeParser.include({ }, }); -return BarcodeParser; +return { + BarcodeParser, + FNC1_CHAR, +}; }); diff --git a/addons/barcodes_gs1_nomenclature/static/src/js/barcode_service.js b/addons/barcodes_gs1_nomenclature/static/src/js/barcode_service.js new file mode 100644 index 00000000000..d37a4adb811 --- /dev/null +++ b/addons/barcodes_gs1_nomenclature/static/src/js/barcode_service.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { session } from "@web/session"; +import { patch } from "@web/core/utils/patch"; +import { barcodeService } from '@barcodes/barcode_service'; + +import { FNC1_CHAR } from "barcodes_gs1_nomenclature/static/src/js/barcode_parser.js"; + + +patch(barcodeService, 'barcodes_gs1_nomenclature', { + // Use the regex given by the session, else use an impossible one + gs1SeparatorRegex: new RegExp(session.gs1_group_separator_encodings || '.^', 'g'), + + cleanBarcode: function(barcode) { + barcode = barcode.replace(barcodeService.gs1SeparatorRegex, FNC1_CHAR); + return this._super(barcode); + }, +}); diff --git a/addons/barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js b/addons/barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js index 5da8f68be94..c5cca71ff7b 100644 --- a/addons/barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js +++ b/addons/barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js @@ -248,35 +248,6 @@ QUnit.module('Barcode GS1 Parser', { }); - QUnit.test('Test Alternative GS1 Separator (fnc1)', async function (assert) { - assert.expect(6); - let barcodeNomenclature = new BarcodeParser({'nomenclature_id': 2}); - await barcodeNomenclature.loaded; - - barcodeNomenclature.nomenclature = this.data['barcode.nomenclature'].records[0]; - barcodeNomenclature.nomenclature.gs1_separator_fnc1 = "#"; - barcodeNomenclature.nomenclature.rules = this.data['barcode.rule'].records; - - // (21)12345(15)090101(16)100101 - let code128 = "2112345\x1D1509010116100101"; - let res; - try { - res = barcodeNomenclature.gs1_decompose_extanded(code128); - } catch (error) { - assert.ok( - error instanceof Error, - "Default separator shouldn't work" - ); - } - - code128 = "2112345#1509010116100101"; - res = barcodeNomenclature.gs1_decompose_extanded(code128); - assert.equal(res.length, 3); - assert.equal(res[0].ai, "21"); - assert.equal(res[0].value, "12345"); - assert.equal(res[1].ai, "15"); - assert.equal(res[2].ai, "16"); - }); }); }); });