mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-19 22:32:02 +02:00
19.0 vanilla
This commit is contained in:
parent
5faf7397c5
commit
2696f14ed7
721 changed files with 220375 additions and 91221 deletions
BIN
odoo-bringout-oca-ocb-barcodes/barcodes/static/img/barcode.png
Normal file
BIN
odoo-bringout-oca-ocb-barcodes/barcodes/static/img/barcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
export class BarcodeHandlerField extends Component {
|
||||
static template = xml``;
|
||||
static props = { ...standardFieldProps };
|
||||
setup() {
|
||||
const barcode = useService("barcode");
|
||||
useBus(barcode.bus, "barcode_scanned", this.onBarcodeScanned);
|
||||
}
|
||||
onBarcodeScanned(event) {
|
||||
const { barcode } = event.detail;
|
||||
this.props.record.update({ [this.props.name]: barcode });
|
||||
}
|
||||
}
|
||||
|
||||
export const barcodeHandlerField = {
|
||||
component: BarcodeHandlerField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("barcode_handler", barcodeHandlerField);
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { getVisibleElements } from "@web/core/utils/ui";
|
||||
import { Macro } from "@web/core/macro";
|
||||
|
||||
const ACTION_HELPERS = {
|
||||
click(el) {
|
||||
el.dispatchEvent(new MouseEvent("mouseover"));
|
||||
el.dispatchEvent(new MouseEvent("mouseenter"));
|
||||
el.dispatchEvent(new MouseEvent("mousedown"));
|
||||
el.dispatchEvent(new MouseEvent("mouseup"));
|
||||
el.click();
|
||||
el.dispatchEvent(new MouseEvent("mouseout"));
|
||||
el.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
},
|
||||
text(el, value) {
|
||||
this.click(el);
|
||||
el.value = value;
|
||||
el.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
||||
el.dispatchEvent(new InputEvent("change", { bubbles: true }));
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
new Macro({
|
||||
name: "updating pager",
|
||||
timeout: 1000,
|
||||
steps: [
|
||||
{
|
||||
trigger: "span.o_pager_value",
|
||||
async action(trigger) {
|
||||
ACTION_HELPERS.click(trigger);
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: "input.o_pager_value",
|
||||
async action(trigger) {
|
||||
ACTION_HELPERS.text(trigger, next);
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start();
|
||||
}
|
||||
|
||||
export const COMMANDS = {
|
||||
"OCDEDIT": () => clickOnButton(".o_form_button_edit"),
|
||||
"OCDDISC": () => clickOnButton(".o_form_button_cancel"),
|
||||
"OCDSAVE": () => clickOnButton(".o_form_button_save"),
|
||||
"OCDPREV": () => clickOnButton(".o_pager_previous"),
|
||||
"OCDNEXT": () => clickOnButton(".o_pager_next"),
|
||||
"OCDPAGERFIRST": () => updatePager("first"),
|
||||
"OCDPAGERLAST": () => 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("OBT")) {
|
||||
let targets = [];
|
||||
try {
|
||||
// the scanned barcode could be anything, and could crash the queryselectorall
|
||||
// function
|
||||
targets = getVisibleElements(ui.activeElement, `[barcode_trigger=${barcode.slice(3)}]`);
|
||||
} catch {
|
||||
console.warn(`Barcode '${barcode}' is not valid`);
|
||||
}
|
||||
for (let elem of targets) {
|
||||
elem.click();
|
||||
}
|
||||
}
|
||||
if (barcode.startsWith("OCD")) {
|
||||
const fn = COMMANDS[barcode];
|
||||
if (fn) {
|
||||
fn();
|
||||
} else {
|
||||
notification.add(_t("Barcode: %(barcode)s", { barcode }), {
|
||||
title: _t("Unknown barcode command"),
|
||||
type: "danger"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("services").add("barcode_handlers", barcodeGenericHandlers);
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { isBrowserChrome, isMobileOS } from "@web/core/browser/feature_detection";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { EventBus, whenReady } from "@odoo/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 || 150,
|
||||
|
||||
// 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") {
|
||||
currentTarget = null;
|
||||
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 && !document.activeElement.matches('input:not([type]), input[type="text"], textarea, [contenteditable], ' +
|
||||
'[type="email"], [type="number"], [type="password"], [type="tel"], [type="search"]')) {
|
||||
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);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { scanBarcode } from "@web/core/barcode/barcode_dialog";
|
||||
import { isBarcodeScannerSupported } from "@web/core/barcode/barcode_video_scanner";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class BarcodeScanner extends Component {
|
||||
static template = "barcodes.BarcodeScanner";
|
||||
static props = {
|
||||
onBarcodeScanned: { type: Function },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.isBarcodeScannerSupported = isBarcodeScannerSupported();
|
||||
this.scanBarcode = () => scanBarcode(this.env, this.facingMode);
|
||||
}
|
||||
|
||||
get facingMode() {
|
||||
return "environment";
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
let error = null;
|
||||
let barcode = null;
|
||||
try {
|
||||
barcode = await this.scanBarcode();
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
|
||||
if (barcode) {
|
||||
this.props.onBarcodeScanned(barcode);
|
||||
if ("vibrate" in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.notification.add(error || _t("Please, Scan again!"), {
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
.o_barcode_mobile_container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 1.08333333rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
|
||||
img, .o_mobile_barcode {
|
||||
width: 115px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.o_mobile_barcode {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
padding-top: 5px;
|
||||
|
||||
.o_barcode_mobile_camera {
|
||||
margin: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_barcode_laser {
|
||||
// avoid dependencies to web file in pos
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -15px;
|
||||
bottom: auto;
|
||||
right: -15px;
|
||||
|
||||
height: 5px;
|
||||
background: rgba(red, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(red, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 576px) {
|
||||
.o_event_barcode_bg.o_home_menu_background {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="barcodes.BarcodeScanner">
|
||||
<div class="o_barcode_mobile_container">
|
||||
<a t-if="isBarcodeScannerSupported" role="button" class="btn btn-primary o_mobile_barcode" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-camera fa-2x o_barcode_mobile_camera"/>
|
||||
Tap to scan
|
||||
</a>
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode"/>
|
||||
<span class="o_barcode_laser"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { BarcodeDialog } from "@web/core/barcode/barcode_dialog";
|
||||
import { Component, onMounted, useRef, useState } from "@odoo/owl";
|
||||
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class BarcodeInput extends Component {
|
||||
static template = "barcodes.BarcodeInput";
|
||||
static props = {
|
||||
onSubmit: Function,
|
||||
placeholder: { type: String, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
placeholder: _t("Enter a barcode..."),
|
||||
};
|
||||
setup() {
|
||||
this.state = useState({
|
||||
barcode: false,
|
||||
});
|
||||
this.barcodeManual = useRef("manualBarcode");
|
||||
// Autofocus processing was blocked because a document already has a focused element.
|
||||
onMounted(() => {
|
||||
this.barcodeManual.el.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when press Enter after filling barcode input manually.
|
||||
*
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydown(ev) {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
if (hotkey === "enter" && this.state.barcode) {
|
||||
this.props.onSubmit(this.state.barcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ManualBarcodeScanner extends BarcodeDialog {
|
||||
static template = "barcodes.ManualBarcodeScanner";
|
||||
static components = {
|
||||
...BarcodeDialog.components,
|
||||
BarcodeInput,
|
||||
};
|
||||
static props = [...BarcodeDialog.props, "placeholder?"];
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="barcodes.BarcodeInput">
|
||||
<div class="input-group">
|
||||
<input id="manual_barcode" t-model="state.barcode" type="text" name="barcode"
|
||||
class="form-control" t-ref="manualBarcode"
|
||||
t-att-placeholder="props.placeholder" t-on-keydown="_onKeydown"/>
|
||||
<button class="input-group-text" t-att-disabled="!state.barcode"
|
||||
t-on-click.prevent="(ev) => this.props.onSubmit(state.barcode)">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="barcodes.ManualBarcodeScanner" t-inherit="web.BarcodeDialog" t-inherit-mode="primary">
|
||||
<Dialog position="attributes">
|
||||
<attribute name="footer">true</attribute>
|
||||
</Dialog>
|
||||
<Dialog position="inside">
|
||||
<t t-set-slot="footer">
|
||||
<BarcodeInput onSubmit="props.onResult" placeholder="props.placeholder"/>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { FloatField, floatField } from "@web/views/fields/float/float_field";
|
||||
|
||||
export class FloatScannableField extends FloatField {
|
||||
static template = "barcodes.FloatScannableField";
|
||||
onBarcodeScanned() {
|
||||
this.inputRef.el.dispatchEvent(new InputEvent("input"));
|
||||
}
|
||||
}
|
||||
|
||||
export const floatScannableField = {
|
||||
...floatField,
|
||||
component: FloatScannableField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("field_float_scannable", floatScannableField);
|
||||
|
|
@ -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">
|
||||
<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>
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
export class BarcodeParser {
|
||||
static barcodeNomenclatureFields = ["name", "rule_ids", "upc_ean_conv"];
|
||||
static barcodeRuleFields = ["name", "sequence", "type", "encoding", "pattern", "alias"];
|
||||
static async fetchNomenclature(orm, id) {
|
||||
const [nomenclature] = await orm.read(
|
||||
"barcode.nomenclature",
|
||||
[id],
|
||||
this.barcodeNomenclatureFields
|
||||
);
|
||||
let rules = await orm.searchRead(
|
||||
"barcode.rule",
|
||||
[["barcode_nomenclature_id", "=", id]],
|
||||
this.barcodeRuleFields
|
||||
);
|
||||
rules = rules.sort((a, b) => {
|
||||
return a.sequence - b.sequence;
|
||||
});
|
||||
nomenclature.rules = rules;
|
||||
return nomenclature;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.setup(...arguments);
|
||||
}
|
||||
|
||||
setup({ nomenclature }) {
|
||||
this.nomenclature = nomenclature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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(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(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(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(barcode) {
|
||||
if (barcode.match(/^urn:/)) {
|
||||
return this.parseURI(barcode);
|
||||
}
|
||||
return this.parseBarcodeNomenclature(barcode);
|
||||
}
|
||||
|
||||
parseBarcodeNomenclature(barcode) {
|
||||
const 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;
|
||||
}
|
||||
|
||||
// URI methods
|
||||
/**
|
||||
* Parse an URI into an object with either the product and its lot/serial
|
||||
* number, either the package.
|
||||
* @param {String} barcode
|
||||
* @returns {Object}
|
||||
*/
|
||||
parseURI(barcode) {
|
||||
const uriParts = barcode.split(":").map(v => v.trim());
|
||||
// URI should be formatted like that (number is the index once split):
|
||||
// 0: urn, 1: epc, 2: id/tag, 3: identifier, 4: data
|
||||
const identifier = uriParts[3];
|
||||
const data = uriParts[4].split(".");
|
||||
if (identifier === "lgtin" || identifier === "sgtin") {
|
||||
return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data);
|
||||
} else if (identifier === "sgtin-96" || identifier === "sgtin-198") {
|
||||
// Same compute then SGTIN but we have to remove the filter.
|
||||
return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data.slice(1));
|
||||
} else if (identifier === "sscc") {
|
||||
return this.convertURISSCCDataIntoPackage(barcode, data);
|
||||
} else if (identifier === "sscc-96") {
|
||||
// Same compute then SSCC but we have to remove the filter.
|
||||
return this.convertURISSCCDataIntoPackage(barcode, data.slice(1));
|
||||
}
|
||||
return barcode;
|
||||
}
|
||||
|
||||
convertURIGTINDataIntoProductAndTrackingNumber(base_code, data) {
|
||||
const [gs1CompanyPrefix, itemRefAndIndicator, trackingNumber] = data;
|
||||
const indicator = itemRefAndIndicator[0];
|
||||
const itemRef = itemRefAndIndicator.slice(1);
|
||||
let productBarcode = indicator + gs1CompanyPrefix + itemRef;
|
||||
productBarcode += this.get_barcode_check_digit(productBarcode + "0");
|
||||
return [
|
||||
{
|
||||
base_code,
|
||||
code: productBarcode,
|
||||
string_value: productBarcode,
|
||||
type: "product",
|
||||
value: productBarcode,
|
||||
}, {
|
||||
base_code,
|
||||
code: trackingNumber,
|
||||
string_value: trackingNumber,
|
||||
type: "lot",
|
||||
value: trackingNumber,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
convertURISSCCDataIntoPackage(base_code, data) {
|
||||
const [gs1CompanyPrefix, serialReference] = data;
|
||||
const extension = serialReference[0];
|
||||
const serialRef = serialReference.slice(1);
|
||||
let sscc = extension + gs1CompanyPrefix + serialRef;
|
||||
sscc += this.get_barcode_check_digit(sscc + "0");
|
||||
return [{
|
||||
base_code,
|
||||
code: sscc,
|
||||
string_value: sscc,
|
||||
type: "package",
|
||||
value: sscc,
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Macro } from "@web/core/macro";
|
||||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, animationFrame, press } from "@odoo/hoot-dom";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mockService,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
async function simulateBarCode(chars) {
|
||||
for (const char of chars) {
|
||||
await press(char);
|
||||
}
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product name" });
|
||||
int_field = fields.Integer({ string: "Integer" });
|
||||
int_field_2 = fields.Integer({ string: "Integer" });
|
||||
barcode = fields.Char({ string: "Barcode" });
|
||||
_records = [
|
||||
{ id: 1, name: "Large Cabinet", barcode: "1234567890" },
|
||||
{ id: 2, name: "Cabinet with Doors", barcode: "0987654321" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
let macro;
|
||||
async function macroIsComplete() {
|
||||
while (!macro.isComplete) {
|
||||
await advanceTime(100);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(Macro.prototype, {
|
||||
start() {
|
||||
super.start(...arguments);
|
||||
macro = this;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Button with barcode_trigger", async () => {
|
||||
mockService("action", {
|
||||
doActionButton: (data) => {
|
||||
expect.step(data.name);
|
||||
},
|
||||
});
|
||||
|
||||
mockService("notification", {
|
||||
add: (params) => {
|
||||
expect.step(params.type);
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form>
|
||||
<header>
|
||||
<button name="do_something" string="Validate" type="object" barcode_trigger="DOIT"/>
|
||||
<button name="do_something_else" string="Validate" type="object" invisible="1" barcode_trigger="DOTHAT"/>
|
||||
</header>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
// OBTDOIT
|
||||
await simulateBarCode(["O", "B", "T", "D", "O", "I", "T", "Enter"]);
|
||||
// OBTDOTHAT (should not call execute_action as the button isn't visible)
|
||||
await simulateBarCode(["O", "B", "T", "D", "O", "T", "H", "A", "T", "Enter"]);
|
||||
expect.verifySteps(["do_something"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Two buttons with same barcode_trigger and the same string and action", async () => {
|
||||
mockService("action", {
|
||||
doActionButton: (data) => {
|
||||
expect.step(data.name);
|
||||
},
|
||||
});
|
||||
|
||||
mockService("notification", {
|
||||
add: (params) => {
|
||||
expect.step(params.type);
|
||||
},
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: `<form>
|
||||
<header>
|
||||
<button name="do_something" string="Validate" type="object" invisible="0" barcode_trigger="DOIT"/>
|
||||
<button name="do_something" string="Validate" type="object" invisible="1" barcode_trigger="DOIT"/>
|
||||
</header>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
});
|
||||
// OBTDOIT should call execute_action as the first button is visible
|
||||
await simulateBarCode(["O", "B", "T", "D", "O", "I", "T", "Enter"]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["do_something"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("edit, save and cancel buttons", async () => {
|
||||
onRpc("web_save", () => expect.step("save"));
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: '<form><field name="name"/></form>',
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
// OCDEDIT
|
||||
await simulateBarCode(["O", "C", "D", "E", "D", "I", "T", "Enter"]);
|
||||
// dummy change to check that it actually saves
|
||||
await contains(".o_field_widget input").edit("test", { confirm: "blur" });
|
||||
|
||||
// OCDSAVE
|
||||
await simulateBarCode(["O", "C", "D", "S", "A", "V", "E", "Enter"]);
|
||||
expect.verifySteps(["save"]);
|
||||
|
||||
// OCDEDIT
|
||||
await simulateBarCode(["O", "C", "D", "E", "D", "I", "T", "Enter"]);
|
||||
// dummy change to check that it correctly discards
|
||||
await contains(".o_field_widget input").edit("test", { confirm: "blur" });
|
||||
// OCDDISC
|
||||
await simulateBarCode(["O", "C", "D", "D", "I", "S", "C", "Enter"]);
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("pager buttons", async () => {
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: '<form><field name="name"/></form>',
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
});
|
||||
|
||||
expect(".o_field_widget input").toHaveValue("Large Cabinet");
|
||||
// OCDNEXT
|
||||
await simulateBarCode(["O", "C", "D", "N", "E", "X", "T", "Enter"]);
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("Cabinet with Doors");
|
||||
|
||||
// OCDPREV
|
||||
await simulateBarCode(["O", "C", "D", "P", "R", "E", "V", "Enter"]);
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("Large Cabinet");
|
||||
|
||||
// OCDPAGERLAST
|
||||
await simulateBarCode(["O", "C", "D", "P", "A", "G", "E", "R", "L", "A", "S", "T", "Enter"]);
|
||||
// need to await 2 macro steps
|
||||
await macroIsComplete();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("Cabinet with Doors");
|
||||
|
||||
// OCDPAGERFIRST
|
||||
await simulateBarCode([
|
||||
"O",
|
||||
"C",
|
||||
"D",
|
||||
"P",
|
||||
"A",
|
||||
"G",
|
||||
"E",
|
||||
"R",
|
||||
"F",
|
||||
"I",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"Enter",
|
||||
]);
|
||||
// need to await 2 macro steps
|
||||
await macroIsComplete();
|
||||
await animationFrame();
|
||||
expect(".o_field_widget input").toHaveValue("Large Cabinet");
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { getActiveElement, queryFirst, keyDown, click } from "@odoo/hoot-dom";
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { barcodeService } from "@barcodes/barcode_service";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(barcodeService, {
|
||||
maxTimeBetweenKeysInMs: 0,
|
||||
isMobileChrome: true,
|
||||
});
|
||||
});
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<form>
|
||||
<input name="email" type="email"/>
|
||||
<input name="number" type="number"/>
|
||||
<input name="password" type="password"/>
|
||||
<input name="tel" type="tel"/>
|
||||
<input name="text"/>
|
||||
<input name="explicit_text" type="text"/>
|
||||
<textarea></textarea>
|
||||
<div contenteditable="true"></div>
|
||||
<select name="select">
|
||||
<option value="option1">Option 1</option>
|
||||
<option value="option2">Option 2</option>
|
||||
</select>
|
||||
</form>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
test.tags("mobile");
|
||||
test("barcode field automatically focus behavior", async () => {
|
||||
expect.assertions(10);
|
||||
await mountWithCleanup(Root);
|
||||
|
||||
// Some elements doesn't need to keep the focus
|
||||
await click(document.body);
|
||||
await keyDown("a");
|
||||
expect(getActiveElement()).toHaveProperty("name", "barcode", {
|
||||
message: "hidden barcode input should have the focus",
|
||||
});
|
||||
|
||||
let element = queryFirst("select");
|
||||
await click(element);
|
||||
await keyDown("b");
|
||||
expect(getActiveElement()).toHaveProperty("name", "barcode", {
|
||||
message: "hidden barcode input should have the focus",
|
||||
});
|
||||
|
||||
// Those elements absolutely need to keep the focus:
|
||||
// inputs elements:
|
||||
const keepFocusedElements = ["email", "number", "password", "tel", "text", "explicit_text"];
|
||||
for (let i = 0; i < keepFocusedElements.length; ++i) {
|
||||
element = queryFirst(`input[name=${keepFocusedElements[i]}]`);
|
||||
await click(element);
|
||||
await keyDown("c");
|
||||
expect(`input[name=${keepFocusedElements[i]}]`).toBeFocused({
|
||||
message: `input ${keepFocusedElements[i]} should keep focus`,
|
||||
});
|
||||
}
|
||||
// textarea element
|
||||
element = queryFirst(`textarea`);
|
||||
await click(element);
|
||||
await keyDown("d");
|
||||
expect(`textarea`).toBeFocused({ message: "textarea should keep focus" });
|
||||
// contenteditable elements
|
||||
element = queryFirst(`[contenteditable=true]`);
|
||||
await click(element);
|
||||
await keyDown("e");
|
||||
expect(`[contenteditable=true]`).toBeFocused({
|
||||
message: "contenteditable should keep focus",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { BarcodeParser } from "@barcodes/js/barcode_parser";
|
||||
|
||||
test.tags("headless");
|
||||
test("Test check digit", async () => {
|
||||
const nomenclature = {
|
||||
id: 1,
|
||||
name: "normal",
|
||||
upc_ean_conv: "always",
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Product Barcodes",
|
||||
barcode_nomenclature_id: 1,
|
||||
sequence: 90,
|
||||
type: "product",
|
||||
encoding: "any",
|
||||
pattern: ".*",
|
||||
},
|
||||
],
|
||||
};
|
||||
const barcodeNomenclature = new BarcodeParser({ nomenclature });
|
||||
|
||||
let ean8 = "87111125";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(ean8)).toEqual(+ean8[ean8.length - 1]);
|
||||
ean8 = "4725992";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(ean8 + "0")).toEqual(8);
|
||||
let ean13 = "1234567891231";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(ean13)).toEqual(+ean13[ean13.length - 1]);
|
||||
ean13 = "962434754318";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(ean13 + "0")).toEqual(4);
|
||||
let utca = "692771981161";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(utca)).toEqual(+utca[utca.length - 1]);
|
||||
utca = "71679131569";
|
||||
expect(barcodeNomenclature.get_barcode_check_digit(utca + "0")).toEqual(7);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { waitFor } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { BarcodeScanner } from "@barcodes/components/barcode_scanner";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
test.tags("desktop");
|
||||
test("Display notification for media device permission on barcode scanning", async () => {
|
||||
navigator.mediaDevices.getUserMedia = function () {
|
||||
return Promise.reject(new DOMException("", "NotAllowedError"));
|
||||
};
|
||||
|
||||
class BarcodeScan extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<BarcodeScanner onBarcodeScanned="(ev) => this.onBarcodeScanned(ev)"/>
|
||||
</div>
|
||||
`;
|
||||
static components = { BarcodeScanner };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(BarcodeScan);
|
||||
await contains("a.o_mobile_barcode").click();
|
||||
await waitFor(".modal-body:contains(camera)");
|
||||
expect(".modal-body").toHaveText(
|
||||
"Unable to access camera\nCould not start scanning. Odoo needs your authorization first."
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { barcodeService } from "@barcodes/barcode_service";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
|
||||
async function simulateBarCode(chars) {
|
||||
for (const char of chars) {
|
||||
await press(char);
|
||||
}
|
||||
}
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product name" });
|
||||
float_field = fields.Float({ string: "Float" });
|
||||
float_field_2 = fields.Float({ string: "Float" });
|
||||
barcode = fields.Char({ string: "Barcode" });
|
||||
_records = [
|
||||
{ id: 1, name: "Large Cabinet", barcode: "1234567890" },
|
||||
{ id: 2, name: "Cabinet with Doors", barcode: "0987654321" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Product]);
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(barcodeService, {
|
||||
maxTimeBetweenKeysInMs: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("widget field_float_scannable", async () => {
|
||||
Product._records[0].float_field = 4;
|
||||
|
||||
const onBarcodeScanned = (event) => {
|
||||
expect.step(`barcode scanned ${event.detail.barcode}`);
|
||||
};
|
||||
|
||||
const view = await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
resId: 1,
|
||||
arch: /*xml*/ `
|
||||
<form>press
|
||||
<field name="display_name"/>
|
||||
<field name="float_field" widget="field_float_scannable"/>
|
||||
<field name="float_field_2"/>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
view.env.services.barcode.bus.addEventListener("barcode_scanned", onBarcodeScanned);
|
||||
|
||||
expect(".o_field_widget[name=float_field] input").toHaveValue("4.00");
|
||||
|
||||
// we check here that a scan on the field_float_scannable widget triggers a
|
||||
// barcode event
|
||||
await contains(".o_field_widget[name=float_field] input").focus();
|
||||
await simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["barcode scanned 60164785"]);
|
||||
expect(".o_field_widget[name=float_field] input").toBeFocused();
|
||||
|
||||
// we check here that a scan on the field without widget does not trigger a
|
||||
// barcode event
|
||||
await contains(".o_field_widget[name=float_field_2] input").focus();
|
||||
await simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"]);
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
|
||||
view.env.services.barcode.bus.removeEventListener("barcode_scanned", onBarcodeScanned);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("do no update form twice after a command barcode scanned", async () => {
|
||||
patchWithCleanup(FormController.prototype, {
|
||||
onPagerUpdate(...args) {
|
||||
expect.step("update");
|
||||
super.onPagerUpdate(...args);
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("web_read", () => {
|
||||
expect.step("web_read");
|
||||
});
|
||||
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "product",
|
||||
arch: /*xml*/ `
|
||||
<form>
|
||||
<field name="display_name"/>
|
||||
<field name="float_field" widget="field_float_scannable"/>
|
||||
</form>
|
||||
`,
|
||||
resId: 1,
|
||||
resIds: [1, 2],
|
||||
});
|
||||
|
||||
expect.verifySteps(["web_read"]);
|
||||
|
||||
// switch to next record
|
||||
await simulateBarCode(["O", "C", "D", "N", "E", "X", "T", "Enter"]);
|
||||
|
||||
// a first update is done to reload the data (thus followed by a read), but
|
||||
// update shouldn't be called afterwards
|
||||
expect.verifySteps(["update", "web_read"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { triggerEvent } from "@web/../tests/helpers/utils";
|
||||
|
||||
export function simulateBarCode(chars, target = document.body, selector = undefined) {
|
||||
for (let char of chars) {
|
||||
triggerEvent(target, selector, "keydown", {
|
||||
key: char,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue