Initial commit: Technical packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 3473fa71a0
873 changed files with 297766 additions and 0 deletions

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { useBus, useService } from "@web/core/utils/hooks";
const { Component, xml } = owl;
export class BarcodeHandlerField extends Component {
setup() {
const barcode = useService("barcode");
useBus(barcode.bus, "barcode_scanned", this.onBarcodeScanned);
}
onBarcodeScanned(event) {
const { barcode } = event.detail;
this.props.update(barcode);
}
}
BarcodeHandlerField.template = xml``;
BarcodeHandlerField.props = { ...standardFieldProps };
registry.category("fields").add("barcode_handler", BarcodeHandlerField);

View file

@ -0,0 +1,92 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { getVisibleElements } from "@web/core/utils/ui";
import { MacroEngine } from "@web/core/macro";
function clickOnButton(selector) {
const button = document.body.querySelector(selector);
if (button) {
button.click();
}
}
function updatePager(position) {
const pager = document.body.querySelector("nav.o_pager");
if (!pager || pager.innerText.includes("-")) {
// we don't change pages if we are in a multi record view
return;
}
let next;
if (position === "first") {
next = 1;
} else {
next = parseInt(pager.querySelector(".o_pager_limit").textContent, 10);
}
let current = parseInt(pager.innerText.split('/')[0], 10);
if (current === next) {
return;
}
const engine = new MacroEngine();
engine.activate({
name: "updating pager",
timeout: 1000,
interval: 0,
steps: [
{
trigger: "span.o_pager_value",
action: "click"
},
{
trigger: "input.o_pager_value",
action: "text",
value: next
}
]
});
}
export const COMMANDS = {
"O-CMD.EDIT": () => clickOnButton(".o_form_button_edit"),
"O-CMD.DISCARD": () => clickOnButton(".o_form_button_cancel"),
"O-CMD.SAVE": () => clickOnButton(".o_form_button_save"),
"O-CMD.PREV": () => clickOnButton(".o_pager_previous"),
"O-CMD.NEXT": () => clickOnButton(".o_pager_next"),
"O-CMD.PAGER-FIRST": () => updatePager("first"),
"O-CMD.PAGER-LAST": () => updatePager("last"),
};
export const barcodeGenericHandlers = {
dependencies: ["ui", "barcode", "notification"],
start(env, { ui, barcode, notification }) {
barcode.bus.addEventListener("barcode_scanned", (ev) => {
const barcode = ev.detail.barcode;
if (barcode.startsWith("O-BTN.")) {
let targets = [];
try {
// the scanned barcode could be anything, and could crash the queryselectorall
// function
targets = getVisibleElements(ui.activeElement, `[barcode_trigger=${barcode.slice(6)}]`);
} catch (_e) {
console.warn(`Barcode '${barcode}' is not valid`);
}
for (let elem of targets) {
elem.click();
}
}
if (barcode.startsWith("O-CMD.")) {
const fn = COMMANDS[barcode];
if (fn) {
fn();
} else {
notification.add(env._t("Barcode: ") + `'${barcode}'`, {
title: env._t("Unknown barcode command"),
type: "danger"
});
}
}
});
}
};
registry.category("services").add("barcode_handlers", barcodeGenericHandlers);

View file

@ -0,0 +1,137 @@
/** @odoo-module **/
import { isBrowserChrome, isMobileOS } from "@web/core/browser/feature_detection";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
const { EventBus, whenReady } = owl;
function isEditable(element) {
return element.matches('input,textarea,[contenteditable="true"]');
}
function makeBarcodeInput() {
const inputEl = document.createElement('input');
inputEl.setAttribute("style", "position:fixed;top:50%;transform:translateY(-50%);z-index:-1;opacity:0");
inputEl.setAttribute("autocomplete", "off");
inputEl.setAttribute("inputmode", "none"); // magic! prevent native keyboard from popping
inputEl.classList.add("o-barcode-input");
inputEl.setAttribute('name', 'barcode');
return inputEl;
}
export const barcodeService = {
// Keys from a barcode scanner are usually processed as quick as possible,
// but some scanners can use an intercharacter delay (we support <= 50 ms)
maxTimeBetweenKeysInMs: session.max_time_between_keys_in_ms || 100,
// 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;
let bufferedBarcode = "";
let currentTarget = null;
let barcodeInput = null;
function handleBarcode(barcode, target) {
bus.trigger('barcode_scanned', {barcode,target});
if (target.getAttribute('barcode_events') === "true") {
const barcodeScannedEvent = new CustomEvent("barcode_scanned", { detail: { barcode, target } });
target.dispatchEvent(barcodeScannedEvent);
}
}
/**
* check if we have a barcode, and trigger appropriate events
*/
function checkBarcode(ev) {
let str = barcodeInput ? barcodeInput.value : bufferedBarcode;
str = barcodeService.cleanBarcode(str);
if (str.length >= 3) {
if (ev) {
ev.preventDefault();
}
handleBarcode(str, currentTarget);
}
if (barcodeInput) {
barcodeInput.value = "";
}
bufferedBarcode = "";
currentTarget = null;
}
function keydownHandler(ev) {
if (!ev.key) {
// Chrome may trigger incomplete keydown events under certain circumstances.
// E.g. when using browser built-in autocomplete on an input.
// See https://stackoverflow.com/questions/59534586/google-chrome-fires-keydown-event-when-form-autocomplete
return;
}
// Ignore 'Shift', 'Escape', 'Backspace', 'Insert', 'Delete', 'Home', 'End', Arrow*, F*, Page*, ...
// 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'
if (isSpecialKey && !isEndCharacter) {
return;
}
currentTarget = ev.target;
// Don't catch events targeting elements that are editable because we
// have no way of redispatching 'genuine' key events. Resent events
// don't trigger native event handlers of elements. So this means that
// our fake events will not appear in eg. an <input> element.
if (currentTarget !== barcodeInput && isEditable(currentTarget) &&
!currentTarget.dataset.enableBarcode &&
currentTarget.getAttribute("barcode_events") !== "true") {
return;
}
clearTimeout(timeout);
if (isEndCharacter) {
checkBarcode(ev);
} else {
bufferedBarcode += ev.key;
timeout = setTimeout(checkBarcode, barcodeService.maxTimeBetweenKeysInMs);
}
}
function mobileChromeHandler(ev) {
if (ev.key === "Unidentified") {
return;
}
if ($(document.activeElement).not('input:text, textarea, [contenteditable], ' +
'[type="email"], [type="number"], [type="password"], [type="tel"], [type="search"]').length) {
barcodeInput.focus();
}
keydownHandler(ev);
}
whenReady(() => {
const isMobileChrome = barcodeService.isMobileChrome;
if (isMobileChrome) {
barcodeInput = makeBarcodeInput();
document.body.appendChild(barcodeInput);
}
const handler = isMobileChrome ? mobileChromeHandler : keydownHandler;
document.body.addEventListener('keydown', handler);
});
return {
bus,
};
},
};
registry.category("services").add("barcode", barcodeService);

View file

@ -0,0 +1,13 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { FloatField } from "@web/views/fields/float/float_field";
export class FloatScannableField extends FloatField {
onBarcodeScanned() {
this.inputRef.el.dispatchEvent(new InputEvent("input"));
}
}
FloatScannableField.template = "barcodes.FloatScannableField";
registry.category("fields").add("field_float_scannable", FloatScannableField);

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="barcodes.FloatScannableField" t-inherit="web.FloatField" t-inherit-mode="primary" owl="1">
<xpath expr="//input" position="attributes">
<attribute name="t-on-barcode_scanned">onBarcodeScanned</attribute>
<attribute name="t-att-data-enable-barcode">true</attribute>
<attribute name="barcode_events">true</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,24 @@
/** @odoo-module **/
/**
* BarcodeEvents has been removed and replaced by the barcode service.
*
* This file is a temporary service to remap barcode events from new barcode
* service to core.bus (which was the purpose of BarcodeEvents).
*
* @TODO: remove this as soon as all barcode code is using new barcode service
*/
import { registry } from "@web/core/registry";
import core from "web.core";
export const barcodeRemapperService = {
dependencies: ["barcode"],
start(env, { barcode }) {
barcode.bus.addEventListener("barcode_scanned", ev => {
const { barcode, target } = ev.detail;
core.bus.trigger('barcode_scanned', barcode, target);
});
},
};
registry.category("services").add("barcode_remapper", barcodeRemapperService);

View file

@ -0,0 +1,61 @@
odoo.define('barcodes.field', function(require) {
"use strict";
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
var FieldFloatScannable = basicFields.FieldFloat.extend({
events: _.extend({}, basicFields.FieldFloat.prototype.events, {
barcode_scanned: '_onBarcodeScan',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_renderEdit: function () {
var self = this;
return Promise.resolve(this._super()).then(function () {
self.$input[0].dataset.enableBarcode = true;
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onBarcodeScan() {
// trigger an 'input' event to make sure that the widget is call
// notifyChanges
this.$input.trigger('input');
}
});
var FormViewBarcodeHandler = AbstractField.extend({
/**
* @override
*/
init: function() {
this._super.apply(this, arguments);
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);
});

View file

@ -0,0 +1,281 @@
odoo.define('barcodes.BarcodeParser', function (require) {
"use strict";
var Class = require('web.Class');
var rpc = require('web.rpc');
// The BarcodeParser is used to detect what is the category
// of a barcode (product, partner, ...) and extract an encoded value
// (like weight, price, etc.)
var BarcodeParser = Class.extend({
init: function(attributes) {
this.nomenclature_id = attributes.nomenclature_id;
this.nomenclature = attributes.nomenclature;
this.loaded = this.load();
},
// This loads the barcode nomenclature and barcode rules which are
// necessary to parse the barcodes. The BarcodeParser is operational
// only when those data have been loaded
load: function(){
if (!this.nomenclature_id) {
return this.nomenclature ? Promise.resolve() : Promise.reject();
}
var id = this.nomenclature_id[0];
return rpc.query({
model: 'barcode.nomenclature',
method: 'read',
args: [[id], this._barcodeNomenclatureFields()],
}).then(nomenclatures => {
this.nomenclature = nomenclatures[0];
var args = [
[['barcode_nomenclature_id', '=', this.nomenclature.id]],
this._barcodeRuleFields(),
];
return rpc.query({
model: 'barcode.rule',
method: 'search_read',
args: args,
});
}).then(rules => {
rules = rules.sort(function(a, b){ return a.sequence - b.sequence; });
this.nomenclature.rules = rules;
});
},
// resolves when the barcode parser is operational.
is_loaded: function() {
return this.loaded;
},
/**
* This algorithm is identical for all fixed length numeric GS1 data structures.
*
* It is also valid for EAN-8, EAN-12 (UPC-A), EAN-13 check digit after sanitizing.
* https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf
*
* @param {String} numericBarcode Need to have a length of 18
* @returns {number} Check Digit
*/
get_barcode_check_digit(numericBarcode) {
let oddsum = 0, evensum = 0, total = 0;
// Reverses the barcode to be sure each digit will be in the right place
// regardless the barcode length.
const code = numericBarcode.split('').reverse();
// Removes the last barcode digit (should not be took in account for its own computing).
code.shift();
// Multiply value of each position by
// N1 N2 N3 N4 N5 N6 N7 N8 N9 N10 N11 N12 N13 N14 N15 N16 N17 N18
// x3 X1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 x1 x3 CHECK_DIGIT
for (let i = 0; i < code.length; i++) {
if (i % 2 === 0) {
evensum += parseInt(code[i]);
} else {
oddsum += parseInt(code[i]);
}
}
total = evensum * 3 + oddsum;
return (10 - total % 10) % 10;
},
/**
* Checks if the barcode string is encoded with the provided encoding.
*
* @param {String} barcode
* @param {String} encoding could be 'any' (no encoding rules), 'ean8', 'upca' or 'ean13'
* @returns {boolean}
*/
check_encoding: function(barcode, encoding) {
if (encoding === 'any') {
return true;
}
const barcodeSizes = {
ean8: 8,
ean13: 13,
upca: 12,
};
return barcode.length === barcodeSizes[encoding] && /^\d+$/.test(barcode) &&
this.get_barcode_check_digit(barcode) === parseInt(barcode[barcode.length - 1]);
},
/**
* Sanitizes a EAN-13 prefix by padding it with chars zero.
*
* @param {String} ean
* @returns {String}
*/
sanitize_ean: function(ean){
ean = ean.substr(0, 13);
ean = "0".repeat(13 - ean.length) + ean;
return ean.substr(0, 12) + this.get_barcode_check_digit(ean);
},
/**
* Sanitizes a UPC-A prefix by padding it with chars zero.
*
* @param {String} upc
* @returns {String}
*/
sanitize_upc: function(upc) {
return this.sanitize_ean(upc).substr(1, 12);
},
// Checks if barcode matches the pattern
// Additionnaly retrieves the optional numerical content in barcode
// Returns an object containing:
// - value: the numerical value encoded in the barcode (0 if no value encoded)
// - base_code: the barcode in which numerical content is replaced by 0's
// - match: boolean
match_pattern: function (barcode, pattern, encoding){
var match = {
value: 0,
base_code: barcode,
match: false,
};
barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\.");
var numerical_content = pattern.match(/[{][N]*[D]*[}]/); // look for numerical content in pattern
var base_pattern = pattern;
if(numerical_content){ // the pattern encodes a numerical content
var num_start = numerical_content.index; // start index of numerical content
var num_length = numerical_content[0].length; // length of numerical content
var value_string = barcode.substr(num_start, num_length-2); // numerical content in barcode
var whole_part_match = numerical_content[0].match("[{][N]*[D}]"); // looks for whole part of numerical content
var decimal_part_match = numerical_content[0].match("[{N][D]*[}]"); // looks for decimal part
var whole_part = value_string.substr(0, whole_part_match.index+whole_part_match[0].length-2); // retrieve whole part of numerical content in barcode
var decimal_part = "0." + value_string.substr(decimal_part_match.index, decimal_part_match[0].length-1); // retrieve decimal part
if (whole_part === ''){
whole_part = '0';
}
match.value = parseInt(whole_part) + parseFloat(decimal_part);
// replace numerical content by 0's in barcode and pattern
match.base_code = barcode.substr(0, num_start);
base_pattern = pattern.substr(0, num_start);
for(var i=0;i<(num_length-2);i++) {
match.base_code += "0";
base_pattern += "0";
}
match.base_code += barcode.substr(num_start + num_length - 2, barcode.length - 1);
base_pattern += pattern.substr(num_start + num_length, pattern.length - 1);
match.base_code = match.base_code
.replace("\\\\", "\\")
.replace("\{", "{")
.replace("\}","}")
.replace("\.",".");
var base_code = match.base_code.split('');
if (encoding === 'ean13') {
base_code[12] = '' + this.get_barcode_check_digit(match.base_code);
} else if (encoding === 'ean8') {
base_code[7] = '' + this.get_barcode_check_digit(match.base_code);
} else if (encoding === 'upca') {
base_code[11] = '' + this.get_barcode_check_digit(match.base_code);
}
match.base_code = base_code.join('');
}
base_pattern = base_pattern.split('|')
.map(part => part.startsWith('^') ? part : '^' + part)
.join('|');
match.match = match.base_code.match(base_pattern);
return match;
},
/**
* Attempts to interpret a barcode (string encoding a barcode Code-128)
*
* @param {string} barcode
* @returns {Object} the returned object containing informations about the barcode:
* - code: the barcode
* - type: the type of the barcode (e.g. alias, unit product, weighted product...)
* - value: if the barcode encodes a numerical value, it will be put there
* - base_code: the barcode with all the encoding parts set to zero; the one put on the product in the backend
*/
parse_barcode: function(barcode){
var parsed_result = {
encoding: '',
type:'error',
code:barcode,
base_code: barcode,
value: 0,
};
if (!this.nomenclature) {
return parsed_result;
}
var rules = this.nomenclature.rules;
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
var cur_barcode = barcode;
if ( rule.encoding === 'ean13' &&
this.check_encoding(barcode,'upca') &&
this.nomenclature.upc_ean_conv in {'upc2ean':'','always':''} ){
cur_barcode = '0' + cur_barcode;
} else if (rule.encoding === 'upca' &&
this.check_encoding(barcode,'ean13') &&
barcode[0] === '0' &&
this.nomenclature.upc_ean_conv in {'ean2upc':'','always':''} ){
cur_barcode = cur_barcode.substr(1,12);
}
if (!this.check_encoding(cur_barcode,rule.encoding)) {
continue;
}
var match = this.match_pattern(cur_barcode, rules[i].pattern, rule.encoding);
if (match.match) {
if(rules[i].type === 'alias') {
barcode = rules[i].alias;
parsed_result.code = barcode;
parsed_result.type = 'alias';
}
else {
parsed_result.encoding = rules[i].encoding;
parsed_result.type = rules[i].type;
parsed_result.value = match.value;
parsed_result.code = cur_barcode;
if (rules[i].encoding === "ean13"){
parsed_result.base_code = this.sanitize_ean(match.base_code);
}
else{
parsed_result.base_code = match.base_code;
}
return parsed_result;
}
}
}
return parsed_result;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_barcodeNomenclatureFields: function () {
return [
'name',
'rule_ids',
'upc_ean_conv',
];
},
_barcodeRuleFields: function () {
return [
'name',
'sequence',
'type',
'encoding',
'pattern',
'alias',
];
},
});
return BarcodeParser;
});