Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions stock_vertical_lift/models/vertical_lift_operation_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,27 +242,21 @@ def reset_steps(self):
self.next_step()

def on_barcode_scanned(self, barcode):
"""React to a barcode scanned on the shuttle screen

Called as a plain RPC by the ``barcode_handler`` widget patched in
``vertical_lift.esm.js``, not through the onchange of the
``barcodes.barcode_events_mixin``: the method has side effects in the
database (update line, go to the next step, ...) and must not leave
pending changes on the form.
"""
self.ensure_one()
# to implement in sub-classes

def on_screen_open(self):
"""Called when the screen is opened"""
self.reset_steps()

def onchange(self, values, field_names, field_onchange):
if "_barcode_scanned" not in field_names:
return super().onchange(values, field_names, field_onchange)

# _barcode_scanner is implemented (in the barcodes module) as an
# onchange, which is really annoying when we want it to act as a
# normal button and actually have side effect in the database
# (update line, go to the next step, ...). This override shorts the
# onchange call and calls the scanner method as a normal method.
self.on_barcode_scanned(values["_barcode_scanned"])
# We can't know which fields on_barcode_scanned changed, refresh
# everything.
return {"value": self.read()[0]}

@api.depends()
def _compute_number_of_ops(self):
for record in self:
Expand Down
96 changes: 86 additions & 10 deletions stock_vertical_lift/static/src/js/vertical_lift.esm.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,74 @@
import {Component, onMounted, onWillUnmount, useRef, useState, xml} from "@odoo/owl";
/* global document, InputEvent */
import {
Component,
onMounted,
onPatched,
onWillUnmount,
useRef,
useState,
xml,
} from "@odoo/owl";
import {BarcodeHandlerField} from "@barcodes/barcode_handler_field";
import {Dialog} from "@web/core/dialog/dialog";
import {FormController} from "@web/views/form/form_controller";
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {Mutex} from "@web/core/utils/concurrency";
import {_t} from "@web/core/l10n/translation";
import {browser} from "@web/core/browser/browser";
import {patch} from "@web/core/utils/patch";
import {registry} from "@web/core/registry";
import {useHotkey} from "@web/core/hotkeys/hotkey_hook";
import {useService} from "@web/core/utils/hooks";

const OPERATION_MODEL_PREFIX = "vertical.lift.operation.";

const SWITCH_BARCODE_METHODS = {
"OBTswitch-pick": "switch_pick",
"OBTswitch-put": "switch_put",
"OBTswitch-inventory": "switch_inventory",
};

// Intercept OBTswitch-* barcodes on vertical lift operation forms before the
// standard barcode_handler forwards them to the model's on_barcode_scanned
// (which would emit "No location found for barcode ...").
// Button (OBT) and command (OCD) barcodes are handled by the generic
// handlers of the barcodes module, the operation must not receive them.
const ACTION_BARCODE_RE = /^(OBT|OCD)/;

// A scan must wait for the previous one to be processed and reloaded,
// otherwise two scans could be evaluated against the same operation step.
// record.update() had this queuing through the form model mutex.
const scanMutex = new Mutex();

function isOperationModel(resModel) {
return resModel.startsWith(OPERATION_MODEL_PREFIX);
}

function blurActiveElement() {
const el = document.activeElement;
if (el && el !== document.body) {
el.blur();
}
}

// The scanner keys typed into the focused input stay in its value: remove
// them once the barcode service confirmed they were a barcode.
function stripScannedBarcode(ev) {
const input = ev.target;
const barcode = ev.detail.barcode;
if (input.value.endsWith(barcode)) {
input.value = input.value.slice(0, -barcode.length);
input.dispatchEvent(new InputEvent("input", {bubbles: true}));
}
}

// Barcode_service.js ignores keys typed while an <input> has the focus unless
// the input carries these attributes, so a scan would be lost.
function enableBarcodeOnInputs(root) {
for (const input of root.querySelectorAll("input:not([barcode_events])")) {
input.setAttribute("barcode_events", "true");
input.dataset.enableBarcode = "true";
input.addEventListener("barcode_scanned", stripScannedBarcode);
}
}

patch(BarcodeHandlerField.prototype, {
setup() {
super.setup();
Expand All @@ -26,15 +77,33 @@ patch(BarcodeHandlerField.prototype, {
},
async onBarcodeScanned(event) {
const barcode = event.detail.barcode;
const {resModel, resId} = this.props.record;
// Intercept OBTswitch-* barcodes on vertical lift operation forms before
// the standard barcode_handler forwards them to the model's
// on_barcode_scanned (which would emit "No location found for barcode").
const method = SWITCH_BARCODE_METHODS[barcode];
if (!method) {
if (method) {
const action = await this.ormService.call(resModel, method, [resId]);
if (action) {
this.actionService.doAction(action);
}
return;
}
if (!isOperationModel(resModel)) {
return super.onBarcodeScanned(event);
}
const {resModel, resId} = this.props.record;
const action = await this.ormService.call(resModel, method, [resId]);
if (action) {
this.actionService.doAction(action);
if (ACTION_BARCODE_RE.test(barcode)) {
return;
}
// Plain RPC + reload instead of record.update(): an update marks the
// form dirty and its pending values (state...) get auto-saved later.
await scanMutex.exec(async () => {
await this.ormService.call(resModel, "on_barcode_scanned", [
resId,
barcode,
]);
await this.props.record.load();
});
},
});

Expand All @@ -61,7 +130,7 @@ patch(FormController.prototype, {
setup() {
super.setup();
this.busService = useService("bus_service");
if (this.props.resModel.startsWith("vertical.lift.operation.")) {
if (isOperationModel(this.props.resModel)) {
this.busService.addChannel("notify_vertical_lift_screen");
this.busService.addEventListener("notification", (notifications) => {
notifications.forEach(([channel, message]) => {
Expand All @@ -73,6 +142,13 @@ patch(FormController.prototype, {
}
});
});
// Enter leaves the focused input (e.g. the inventory quantity).
useHotkey("enter", () => blurActiveElement(), {
bypassEditableProtection: true,
});
const enableBarcode = () => enableBarcodeOnInputs(this.rootRef.el);
onMounted(enableBarcode);
onPatched(enableBarcode);
}

onWillUnmount(() => {
Expand Down
Loading