Initial commit: OCA Warehouse packages (12 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit af1eea7692
627 changed files with 55555 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,135 @@
@mixin barcode-decoration() {
i.fa-barcode {
font-size: 2em !important;
@include media-breakpoint-down(sm) {
font-size: 3em !important;
}
}
}
div.o_kanban_renderer {
button[name="action_barcode_scan"] {
@include barcode-decoration;
}
}
div.o_kanban_stock_barcodes {
padding: 10px !important;
button.o_stock_mobile_barcode {
@include barcode-decoration;
}
button.o_stock_mobile_barcode:focus {
box-shadow: none !important;
}
}
div.alert.barcode-info {
background-color: $o-community-color !important;
span.fa-barcode {
margin: 0.5rem 1rem 0 1rem !important;
@include media-breakpoint-down(sm) {
margin: 0 0 0 5px !important;
font-size: 1em !important;
}
}
}
.inventory_quant_ids_with_form {
height: 710px !important;
@include media-breakpoint-down(sm) {
height: 500px !important;
}
}
.inventory_quant_ids_without_form {
height: 822px !important;
@include media-breakpoint-down(sm) {
height: 648px !important;
}
}
div.oe_kanban_picking_done {
background-color: #353840 !important;
border: none !important;
box-shadow: none !important;
height: 230px !important;
}
div[name="inventory_quant_ids"],
div[name="pending_move_ids"],
div[name="move_line_ids"] {
div.o_kanban_renderer {
padding: 0 !important;
&:has(div.oe_kanban_picking_done) {
height: 50% !important;
}
div.o_kanban_record {
box-shadow: rgba(0, 0, 0, 0.35) 0 5px 15px !important;
i.fa-pencil,
i.fa-trash {
font-size: 3.5em !important;
}
img,
span.text-end.fw-bold {
margin-right: 5% !important;
}
div.indent {
text-indent: 5px !important;
}
}
button.btn-op-rest,
button.btn-op-sum {
background-color: $o-community-color !important;
min-width: 55px !important;
height: 60px !important;
padding: 12px 8px !important;
border-radius: 8px !important;
line-height: 16px !important;
font-size: 16px !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-transform: none;
}
}
}
button[name="action_clean_product"],
button[name="action_clean_lot"],
button#btn_create_lot {
width: 5% !important;
padding: 0.9rem !important;
@include media-breakpoint-down(sm) {
width: 95% !important;
margin-top: 0.5em !important;
padding: 0.3rem !important;
i.o_button_icon {
font-size: 1.5em !important;
}
}
}
div.stock_barcodes_action_kanban {
div.o_kanban_record {
div.oe_kanban_content {
padding: 1.5rem 1.5rem 1.5rem 0.5rem !important;
div.count-elements {
border: 1px solid;
padding: 1px 4px 1px 4px !important;
border-radius: 40% !important;
background-color: lightgray !important;
}
}
}
}

View file

@ -0,0 +1,285 @@
@mixin margin-form-edit-sm($margin) {
@include media-breakpoint-down(sm) {
margin: $margin !important;
}
}
.oe_stock_scan_button {
border: none !important;
background: none !important;
box-shadow: none !important;
}
.oe_stock_barcodes_bottombar {
bottom: 0;
background-color: $o-view-background-color;
border-width: 1px 0 0 0 solid $border-color;
box-shadow: 0 -3px 10px #c9ccd2;
height: 60px !important;
}
// Avoid too big small buttons from core
.o_web_client.o_touch_device {
.oe_stock_barcordes_form {
.btn {
&,
.btn-sm {
padding: 0.25rem 0.5rem;
}
}
}
}
.oe_stock_barcordes_form {
padding: 0 !important;
height: 100%;
// Recover useless space
div[name="_barcode_scanned"] {
min-height: 0 !important;
}
div[name="package_id"],
div[name="product_id"],
div[name="lot_id"] {
width: 90% !important;
}
div[name="product_id"],
div[name="package_id"],
div[name="lot_name"] {
@include margin-form-edit-sm(0 0 0 1%);
}
div[name="location_dest_id"] {
@include margin-form-edit-sm(0 0 1% 1%);
}
div.widget_numeric_step {
font-size: 1.5rem !important;
}
input#location_id,
input#location_dest_id,
input#package_id,
input#product_id,
input#lot_id_1,
input#lot_name {
border-radius: 0.5rem !important;
padding: 0.375rem 0.75rem !important;
height: 40px !important;
font-size: 1.5rem !important;
& + ul.o-autocomplete--dropdown-menu {
li {
font-size: 1.5rem !important;
}
}
}
div[name="candidate_picking_ids"] {
div.oe_kanban_color_alert {
padding: 0 !important;
margin: 0 !important;
}
div.o_kanban_ungrouped.o_kanban_renderer {
margin: 0 !important;
padding: 0 !important;
}
@include media-breakpoint-down(sm) {
div.o_kanban_renderer {
margin: 2% 1% 2% 1% !important;
}
}
}
div.scan_fields {
width: 100% !important;
margin: 0 !important;
div.o-autocomplete.dropdown {
+ a.o_dropdown_button {
display: none !important;
}
}
@include media-breakpoint-down(sm) {
padding: 0 2% 0 2% !important;
width: 100% !important;
}
> div.o_inner_group.grid.col-lg-6 {
div.o_cell {
width: 100% !important;
}
}
div.mt4.col-lg-6 {
@include media-breakpoint-down(sm) {
margin-bottom: 1.5rem !important;
}
}
&:has(button[name="action_clean_lot"]) {
div[name="lot_name"] {
width: 88% !important;
@include media-breakpoint-down(sm) {
width: 90% !important;
}
}
button[name="action_clean_lot"] {
margin-left: 5px !important;
}
}
}
.o_group .scan_fields {
&.o_inner_group {
margin-bottom: 0 !important;
}
@include media-breakpoint-down(sm) {
padding: 2% 0 2% 0 !important;
}
margin: 0 !important;
}
.o_form_sheet,
.o_form_sheet_bg {
padding: 0 !important;
margin: 0 !important;
max-width: 100% !important;
border: 0 !important;
}
// In Odoo 16 the flat input styling lacks proper usability
.o_field_widget {
margin-bottom: 1px !important;
.o_input {
border-radius: 3px;
border-width: 1px;
background-color: white;
}
.o_x2m_control_panel {
margin: 0px !important;
}
}
.o_kanban_record {
flex-basis: 100%;
.btn-full-width {
margin: -9px;
width: calc(100% + 18px);
height: calc(100% + 18px);
}
&.o_kanban_ghost {
display: none;
}
}
.alert {
//position: fixed;
top: 0;
width: 100%;
border-radius: 0;
padding: 0;
min-height: 50px;
z-index: 999;
}
.oe_stock_barcordes_content {
overflow-y: overlay !important;
div.g-col-sm-2 {
&:has(div.o_horizontal_separator) {
display: none !important;
}
}
div.o_inner_group.grid.px-3 {
padding: 0 !important;
}
div[name="picking_id"] {
> a.o_form_uri {
span {
color: white !important;
}
}
}
div[name="action_unlock_picking"] {
span {
color: white !important;
}
}
}
div[name="info"] {
div.alert {
display: flex !important;
@include media-breakpoint-down(sm) {
display: block !important;
text-align: center !important;
}
}
div.barcode-danger {
background-color: #dc3545 !important;
}
}
}
.o_kanban_barcode {
.o_kanban_record.oe_kanban_details {
@extend .btn;
@extend .btn-secondary;
padding: 0.6em 0;
margin-bottom: 0.5em;
}
}
.oe_kanban_action_button:focus {
background-color: lightgray;
}
// Left icon in small screens
.oe_span_small_icon {
width: 25px;
text-align: center;
}
// Display 100% all menu elements
.oe_kanban_card_full_width {
width: 100% !important;
}
// The kanban view adds some pre-styles that we want to be able to tweak
div[name="menu_actions"] {
div[role="article"] {
margin-top: 10px !important;
}
}
// Dropdown that is desactivated at lg width
@media (min-width: 992px) {
.d-lg-flex-no-dropdown {
position: relative !important;
display: flex !important;
border: none;
box-shadow: none;
bottom: auto !important;
transform: none !important;
}
}
.dropdown-menu.d-lg-flex-no-dropdown {
.d-flex {
margin-bottom: 5px;
}
}

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import {BarcodeHandlerField} from "@barcodes/barcode_handler_field";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
const {useEffect} = owl;
patch(BarcodeHandlerField.prototype, "stock_barcodes.BarcodeHandlerField", {
/* eslint-disable no-unused-vars */
setup() {
this._super(...arguments);
const busService = useService("bus_service");
this.orm = useService("orm");
const notifyChanges = async ({detail: notifications}) => {
for (const {payload, type} of notifications) {
if (type === "stock_barcodes_refresh_data") {
await this.env.model.root.load();
this.env.model.notify();
}
}
};
useEffect(() => {
busService.addChannel("barcode_reload");
busService.addEventListener("notification", notifyChanges);
return () => {
busService.deleteChannel("barcode_reload");
busService.removeEventListener("notification", notifyChanges);
};
});
},
onBarcodeScanned(event) {
this._super(...arguments);
if (this.props.record.resModel.includes("wiz.stock.barcodes.read")) {
$("#dummy_on_barcode_scanned").click();
}
},
});

View file

@ -0,0 +1,26 @@
/** @odoo-module */
/* Copyright 2022 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
// Models allowed to have extra keybinding features
export const barcodeModels = [
"stock.barcodes.action",
"stock.picking",
"stock.picking.type",
"wiz.candidate.picking",
"wiz.stock.barcodes.new.lot",
"wiz.stock.barcodes.read",
"wiz.stock.barcodes.read.inventory",
"wiz.stock.barcodes.read.picking",
"wiz.stock.barcodes.read.todo",
];
/**
* Helper to know if the given model is allowed
*
* @param {String} modelName
* @returns {Boolean}
*/
export function isAllowedBarcodeModel(modelName) {
return barcodeModels.includes(modelName);
}

View file

@ -0,0 +1,88 @@
/** @odoo-module **/
import {_t} from "@web/core/l10n/translation";
import {browser} from "@web/core/browser/browser";
import {markup} from "@odoo/owl";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
const {Component, onWillStart, useEffect} = owl;
export class StockBarcodesMainMenu extends Component {
setup() {
super.setup();
this.actionService = useService("action");
this.ormService = useService("orm");
const busService = useService("bus_service");
const notification = useService("notification");
this.modelBarcodeAction = "stock.barcodes.action";
if (this.hasService("home_menu"))
this.homeMenuService = useService("home_menu");
onWillStart(async () => {
this.barcodeActions = await this.getBarcodeActions();
});
const handleNotification = ({detail: notifications}) => {
if (notifications && notifications.length > 0) {
notifications.forEach((notif) => {
const {payload, type} = notif;
if (type === "actions_main_menu_barcode") {
if (payload.action_ok && payload.action) {
this.actionService.doAction(payload.action);
} else {
notification.add(
_t("No action found with barcode: " + payload.barcode),
{
type: "danger",
}
);
}
}
});
}
};
useEffect(() => {
busService.addChannel("stock_barcodes_main_menu");
busService.addEventListener("notification", handleNotification);
return () => {
busService.deleteChannel("stock_barcodes_main_menu");
busService.removeEventListener("notification", handleNotification);
};
});
}
hasService(service) {
return service in this.env.services;
}
mainMenuHome() {
// Enterprise
if (this.hasService("home_menu")) {
this.homeMenuService.toggle(true);
} else {
// Community
this.actionService.doAction("mail.action_discuss");
browser.setTimeout(() => browser.location.reload(), 100);
}
}
async openAction(action_id) {
const action = await this.ormService.call(
this.modelBarcodeAction,
"open_action",
[action_id]
);
action.help = markup(_t(action.help));
this.actionService.doAction(action);
}
async getBarcodeActions() {
return await this.ormService.call(this.modelBarcodeAction, "search_read", [], {
domain: [["action_window_id", "!=", false]],
fields: ["id", "name", "icon_class"],
});
}
}
StockBarcodesMainMenu.template = "stock_barcodes.MainMenu";
registry.category("actions").add("stock_barcodes_main_menu", StockBarcodesMainMenu);

View file

@ -0,0 +1,74 @@
@keyframes o_barcode_scanner_intro {
25% {
top: 75%;
}
50% {
top: 0;
}
75% {
top: 100%;
}
100% {
top: 50%;
}
}
div.o_action_manager {
&:has(div.stock-barcodes-main-menu) {
overflow-y: scroll !important;
background-color: $o-community-color !important;
@include media-breakpoint-down(sm) {
overflow: scroll !important;
}
}
}
div.stock-barcodes-main-menu {
background-color: white !important;
margin: 0 10% 5% 10% !important;
border-radius: 5px !important;
min-height: 90% !important;
@include media-breakpoint-down(sm) {
margin: 0 !important;
}
img {
height: 220px !important;
}
div.o_stock_barcode_functions {
margin-top: 5rem;
@include media-breakpoint-down(sm) {
margin-top: 3.6rem;
}
}
div.o_stock_barcode_buttons {
button {
padding: 1.5rem 1.5rem !important;
}
}
span.o_stock_barcode_laser {
@include o-position-absolute(33%, -15px, auto, -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;
width: 26%;
margin-left: 38%;
@include media-breakpoint-down(sm) {
@include o-position-absolute(35%, -15px, auto, -15px);
width: 95%;
margin-left: 6%;
}
}
div.o_stock_barcode_header_home {
padding-right: 45% !important;
@include media-breakpoint-down(sm) {
padding-right: 27% !important;
}
}
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div
t-name="stock_barcodes.MainMenu"
class="d-flex flex-column stock-barcodes-main-menu align-items-center p-4"
owl="1"
>
<div
class="d-flex o_stock_barcode_header_home justify-content-between align-items-center w-100"
>
<a href="#" t-on-click="mainMenuHome">
<i class="fa fa-chevron-left fa-2x" />
</a>
<h1 class="mb-4">Barcode Scanner</h1>
</div>
<div
class="alert alert-info alert-dismissible fade show w-100 fs-3 o_stock_barcode_description"
role="alert"
>
Scan a barcode actions
</div>
<div
class="d-flex justify-content-center w-100 mt-3 px-2 o_stock_barcode_buttons"
>
<div class="row w-100">
<t
t-foreach="this.barcodeActions"
t-as="barcodeAction"
t-key="barcodeAction.id"
>
<div class="col-12 col-md-6">
<button
class="btn btn-primary btn-lg w-100 mt-3 text-uppercase"
t-on-click="() => this.openAction(barcodeAction.id)"
>
<span t-attf-class="#{barcodeAction.icon_class} mx-2" />
<t t-out="barcodeAction.name" />
</button>
</div>
</t>
</div>
</div>
</div>
</templates>

View file

@ -0,0 +1,71 @@
/** @odoo-module */
/* Copyright 2021 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {onMounted, useEffect} from "@odoo/owl";
import {FormController} from "@web/views/form/form_controller";
import {useService} from "@web/core/utils/hooks";
export class StockBarcodesFormController extends FormController {
setup() {
super.setup();
const busService = useService("bus_service");
const ormService = useService("orm");
this.enableApplyCount = false;
// Adds support to use control_pannel_hidden from the
// context to disable the control panel
if (this.props.context.control_panel_hidden) {
this.display.controlPanel = false;
}
const handleNotification = ({detail: notifications}) => {
if (notifications && notifications.length > 0) {
notifications.forEach((notif) => {
const {payload, type} = notif;
if (type === "count_apply_inventory" && payload) {
this.countApplyInventory(payload.count);
}
});
}
};
useEffect(() => {
busService.addChannel("stock_barcodes_form_update");
busService.addEventListener("notification", handleNotification);
const $applyInventory = $("span.count_apply_inventory");
if ($applyInventory.length > 0) {
if (!this.enableApplyCount) {
this.countApplyInventory(1);
this.enableApplyCount = true;
}
} else {
this.enableApplyCount = false;
}
return () => {
busService.deleteChannel("stock_barcodes_form_update");
busService.removeEventListener("notification", handleNotification);
};
});
onMounted(async () => {
if (this.props.resModel === "wiz.stock.barcodes.read.inventory") {
const fields = ["count_inventory_quants"];
const countApply = await ormService.call(
this.props.resModel,
"read",
[this.props.resId],
{fields}
);
this.countApplyInventory(
countApply.length > 0 ? countApply[0].count_inventory_quants : 0
);
}
});
}
countApplyInventory(countApply = 0) {
const $countApply = $("span.count_apply_inventory");
if ($countApply.length) {
$countApply.text(countApply);
}
}
}

View file

@ -0,0 +1,14 @@
/** @odoo-module */
/* Copyright 2021 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {StockBarcodesFormController} from "./form_controller.esm";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
export const StockBarcodesFormView = {
...formView,
Controller: StockBarcodesFormController,
};
registry.category("views").add("stock_barcodes_form", StockBarcodesFormView);

View file

@ -0,0 +1,27 @@
/** @odoo-module */
/* Copyright 2022 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {KanbanRecord} from "@web/views/kanban/kanban_record";
import {patch} from "@web/core/utils/patch";
patch(KanbanRecord.prototype, "stock_barcodes.KanbanRecord", {
props: {
...KanbanRecord.props,
},
setup() {
this._super(...arguments);
},
async onCustomGlobalClick() {
const record_barcode = $('div[name="inventory_quant_ids"]');
if (record_barcode.length > 0) {
const record = this.props.record;
$("div.oe_kanban_operations").addClass("d-none");
$("div.oe_kanban_operations-" + record.data.id).removeClass("d-none");
return;
}
this._super.apply(this, arguments);
},
});

View file

@ -0,0 +1,200 @@
/** @odoo-module */
/* Copyright 2022 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {onPatched, useEffect, useRef} from "@odoo/owl";
import {useBus, useService} from "@web/core/utils/hooks";
import {KanbanRenderer} from "@web/views/kanban/kanban_renderer";
import {isAllowedBarcodeModel} from "../../utils/barcodes_models_utils.esm";
import {patch} from "@web/core/utils/patch";
import {useHotkey} from "@web/core/hotkeys/hotkey_hook";
patch(KanbanRenderer.prototype, "stock_barcodes.KanbanRenderer", {
setup() {
const rootRef = useRef("root");
useHotkey(
"Enter",
({target}) => {
if (!target.classList.contains("o_kanban_record")) {
return;
}
// Open first link
let firstLink = null;
if (isAllowedBarcodeModel(this.props.list.resModel)) {
firstLink = target.querySelector(
".oe_kanban_action_button,.oe_btn_quick_action"
);
}
if (!firstLink) {
firstLink = target.querySelector(
".oe_kanban_global_click, a, button"
);
}
if (firstLink && firstLink instanceof HTMLElement) {
firstLink.click();
}
return;
},
{area: () => rootRef.el}
);
this._super(...arguments);
this.ormService = useService("orm");
this.action = useService("action");
const busService = useService("bus_service");
this.enableCurrentOperation = 0;
const handleNotification = ({detail: notifications}) => {
if (notifications && notifications.length > 0) {
notifications.forEach((notif) => {
const {payload, type} = notif;
if (type === "enable_operations" && payload) {
this.enableCurrentOperation = payload.id;
}
});
}
};
useEffect(() => {
busService.addChannel("stock_barcodes_kanban_update");
busService.addEventListener("notification", handleNotification);
return () => {
busService.deleteChannel("stock_barcodes_kanban_update");
busService.removeEventListener("notification", handleNotification);
};
});
onPatched(() => {
$("div.oe_kanban_operations-" + this.enableCurrentOperation).removeClass(
"d-none"
);
});
if (isAllowedBarcodeModel(this.props.list.resModel)) {
if (this.env.searchModel) {
useBus(this.env.searchModel, "focus-view", () => {
const {model} = this.props.list;
if (model.useSampleModel || !model.hasData()) {
return;
}
const cards = Array.from(
rootRef.el.querySelectorAll(".o_kanban_record")
);
const firstCard = cards.find(
(card) =>
card.querySelectorAll("button[name='action_barcode_scan']")
.length > 0
);
if (firstCard) {
// Focus first kanban card
firstCard.focus();
}
});
}
}
this.showMessageScanProductPackage =
this.props.list.resModel === "stock.picking";
},
getNextCard(direction, iCard, cards, iGroup, isGrouped) {
let nextCard = null;
switch (direction) {
case "down":
nextCard = iCard < cards[iGroup].length - 1 && cards[iGroup][iCard + 1];
break;
case "up":
nextCard = iCard > 0 && cards[iGroup][iCard - 1];
break;
case "right":
if (isGrouped) {
nextCard = iGroup < cards.length - 1 && cards[iGroup + 1][0];
} else {
nextCard = iCard < cards[0].length - 1 && cards[0][iCard + 1];
}
break;
case "left":
if (isGrouped) {
nextCard = iGroup > 0 && cards[iGroup - 1][0];
} else {
nextCard = iCard > 0 && cards[0][iCard - 1];
}
break;
}
return nextCard;
},
// eslint-disable-next-line complexity
// This is copied from the base kanban_renderer.
// We want to only focus card with barcode when isAllowedBarcodeModel returns true
// Since there is no way to hook and change the candidate cards that are selectable
// (cards line 84) we cannot inherit and change the result. And even if we called
// super it would not respect inheritability
/**
* Redefines focusNextCard to select only kanban card with a barcode
* when isAllowBarcodeModel returns true for the current model
*
* @param {Node} area
* @param {String} direction
*
* @returns {String/Boolean}
*/
focusNextCard(area, direction) {
const {isGrouped} = this.props.list;
const closestCard = document.activeElement.closest(".o_kanban_record");
if (!closestCard) {
return;
}
const groups = isGrouped
? [...area.querySelectorAll(".o_kanban_group")]
: [area];
let cards = [...groups]
.map((group) => [...group.querySelectorAll(".o_kanban_record")])
.filter((group) => group.length);
if (isAllowedBarcodeModel(this.props.list.resModel)) {
cards = cards.map((group) => {
const result = group.filter((card) => {
return (
card.querySelectorAll('button[name="action_barcode_scan"]')
.length > 0
);
});
return result;
});
}
let iGroup = null;
let iCard = null;
for (iGroup = 0; iGroup < cards.length; iGroup++) {
const i = cards[iGroup].indexOf(closestCard);
if (i !== -1) {
iCard = i;
break;
}
}
if (iCard === undefined) {
iCard = 0;
iGroup = 0;
}
// Find next card to focus
const nextCard = this.getNextCard(direction, iCard, cards, iGroup, isGrouped);
if (nextCard && nextCard instanceof HTMLElement) {
nextCard.focus();
return true;
}
},
async openBarcodeScanner() {
if (this.showMessageScanProductPackage) {
const action = await this.ormService.call(
"stock.picking",
"action_barcode_scan",
[false, false]
);
this.action.doAction(action);
}
},
});
KanbanRenderer.template = "stock_barcodes.BarcodeKanbanRenderer";

View file

@ -0,0 +1,8 @@
/** @odoo-module */
import {kanbanView} from "@web/views/kanban/kanban_view";
import {registry} from "@web/core/registry";
registry.category("views").add("stock_barcodes_kanban", {
...kanbanView,
});

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="stock_barcodes.BarcodeKanbanRenderer"
t-inherit="web.KanbanRenderer"
owl="1"
>
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<div
t-if="showMessageScanProductPackage"
class="o_kanban_stock_barcodes text-white w-100 mt-1 mb-1 d-flex align-items-center justify-content-center bg-dark"
>
<span t-if="packageEnabled">Scan a <b>transfer</b>, a <b
>product</b> or a <b>lot</b> to filter your records</span>
<span t-else="">Scan a <b>transfer</b> or a <b
>product</b> to filter your records</span>
<button
class="o_stock_mobile_barcode btn"
t-on-click="openBarcodeScanner"
>
<i class="fa fa-barcode" />
</button>
</div>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,16 @@
/** @odoo-module */
import {ViewCompiler} from "@web/views/view_compiler";
import {patch} from "@web/core/utils/patch";
patch(ViewCompiler.prototype, "Add hotkey props to button tag", {
compileButton(el, params) {
const hotkey = el.getAttribute("data-hotkey");
el.removeAttribute("data-hotkey");
const button = this._super(el, params);
if (hotkey) {
button.setAttribute("hotkey", hotkey);
}
return button;
},
});

View file

@ -0,0 +1,225 @@
/** @odoo-module */
/* Copyright 2024 Akretion
/* Copyright 2024 Tecnativa
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {getVisibleElements, isVisible} from "@web/core/utils/ui";
import {FormController} from "@web/views/form/form_controller";
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {ListController} from "@web/views/list/list_controller";
import {_t} from "@web/core/l10n/translation";
import {isAllowedBarcodeModel} from "../utils/barcodes_models_utils.esm";
import {patch} from "@web/core/utils/patch";
import {useEffect} from "@odoo/owl";
import {useService} from "@web/core/utils/hooks";
let barcodeOverlaysVisible = false;
// This is necessary because the hotkey service does not make its API public for
// some reasons
export function barcodeRemoveHotkeyOverlays() {
for (const overlay of document.querySelectorAll(".o_barcode_web_hotkey_overlay")) {
overlay.remove();
}
barcodeOverlaysVisible = false;
}
// This is necessary because the hotkey service does not make its API public for
// some reasons
export function barcodeAddHotkeyOverlays(activeElement) {
for (const el of getVisibleElements(
activeElement,
"[data-hotkey]:not(:disabled)"
)) {
const hotkey = el.dataset.hotkey;
const overlay = document.createElement("div");
overlay.classList.add(
"o_barcode_web_hotkey_overlay",
"position-absolute",
"top-0",
"bottom-0",
"start-0",
"end-0",
"d-flex",
"justify-content-center",
"align-items-center",
"m-0",
"bg-black-50",
"h6"
);
const overlayKbd = document.createElement("kbd");
overlayKbd.className = "small";
overlayKbd.appendChild(document.createTextNode(hotkey.toUpperCase()));
overlay.appendChild(overlayKbd);
let overlayParent = null;
if (el.tagName.toUpperCase() === "INPUT") {
// Special case for the search input that has an access key
// defined. We cannot set the overlay on the input itself,
// only on its parent.
overlayParent = el.parentElement;
} else {
overlayParent = el;
}
if (overlayParent.style.position !== "absolute") {
overlayParent.style.position = "relative";
}
overlayParent.appendChild(overlay);
}
barcodeOverlaysVisible = true;
}
function setupView() {
const actionService = useService("action");
const uiService = useService("ui");
const busService = useService("bus_service");
const notification = useService("notification");
const handleKeys = async (ev) => {
if (ev.keyCode === 113) {
// F2
const {activeElement} = uiService;
if (barcodeOverlaysVisible) {
barcodeRemoveHotkeyOverlays();
} else {
barcodeAddHotkeyOverlays(activeElement);
}
} else if (ev.keyCode === 120) {
// F9
const button = document.querySelector("button[name='action_clean_values']");
if (isVisible(button)) {
button.click();
}
} else if (ev.keyCode === 123 || ev.keyCode === 115) {
// F12 or F4
await actionService.doAction(
"stock_barcodes.action_stock_barcodes_action_client",
{
name: "Barcode wizard menu",
res_model: "wiz.stock.barcodes.read.picking",
type: "ir.actions.act_window",
}
);
}
};
const handleNotification = ({detail: notifications}) => {
if (notifications && notifications.length > 0) {
notifications.forEach((notif) => {
const {payload, type} = notif;
if (
(this.model.root.resModel === payload.res_model) &
(this.model.root.resId === payload.res_id)
) {
if (type === "stock_barcodes_sound") {
if (payload.sound === "ko") {
this.$sound_ko[0].play();
} else {
this.$sound_ok[0].play();
}
} else if (type === "stock_barcodes_focus") {
requestIdleCallback(() => {
const input = document.querySelector(
`[name=${payload.field_name}] input`
);
if (input) {
input.focus();
}
});
} else if (type === "stock_barcodes_notify") {
notification.add(notif.payload.message, {
title: notif.payload.title,
type: notif.payload.type,
sticky: notif.payload.sticky,
});
}
}
if (type === "stock_barcodes_edit_manual") {
if (payload.manual_entry) {
this.env.bus.trigger("enableFormEditBarcode");
} else if (!payload.manual_entry) {
this.env.bus.trigger("disableFormEditBarcode");
}
} else if (type === "actions_barcode") {
if (payload.valid_picking) {
notification.add(_t("The transfer has been validated"), {
type: "success",
});
} else if (payload.apply_inventory) {
actionService.doAction(
"stock_barcodes.action_stock_barcodes_action_client"
);
notification.add(
_t("The inventory adjustment has been validated"),
{
type: "success",
}
);
}
} else if (type === "actions_barcode_notification") {
notification.add(_t(payload.message), {
type: payload.message_type,
sticky: payload.sticky,
});
}
});
}
};
useEffect(() => {
document.body.addEventListener("keydown", handleKeys);
this.$sound_ok = $("<audio>", {
src: "/stock_barcodes/static/src/sounds/bell.wav",
preload: "auto",
});
this.$sound_ok.appendTo("body");
this.$sound_ko = $("<audio>", {
src: "/stock_barcodes/static/src/sounds/error.wav",
preload: "auto",
});
this.$sound_ko.appendTo("body");
busService.addChannel("stock_barcodes_scan");
busService.addEventListener("notification", handleNotification);
return () => {
this.$sound_ok.remove();
this.$sound_ko.remove();
document.body.removeEventListener("keydown", handleKeys);
busService.deleteChannel("stock_barcodes_scan");
busService.removeEventListener("notification", handleNotification);
};
});
}
patch(KanbanController.prototype, "add hotkeys to kanban", {
setup() {
this._super(...arguments);
if (isAllowedBarcodeModel(this.props.resModel)) {
setupView.call(this);
}
},
});
patch(FormController.prototype, "add hotkeys to form", {
setup() {
this._super(...arguments);
if (isAllowedBarcodeModel(this.props.resModel)) {
setupView.call(this);
}
},
});
patch(ListController.prototype, "add hotkeys to list", {
setup() {
this._super(...arguments);
if (isAllowedBarcodeModel(this.props.resModel)) {
setupView.call(this);
}
},
});

View file

@ -0,0 +1,72 @@
/** @odoo-module */
/* Copyright 2018-2019 Sergio Teruel <sergio.teruel@tecnativa.com>.
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {BooleanToggleField} from "@web/views/fields/boolean_toggle/boolean_toggle_field";
import {onMounted} from "@odoo/owl";
import {registry} from "@web/core/registry";
import {useBus} from "@web/core/utils/hooks";
class BarcodeBooleanToggleField extends BooleanToggleField {
setup() {
super.setup();
onMounted(() => {
this.enableFormEdit(this.props.value, true);
});
useBus(this.env.bus, "enableFormEditBarcode", () =>
this.enableFormEdit(true, true)
);
useBus(this.env.bus, "disableFormEditBarcode", () =>
this.enableFormEdit(false, true)
);
}
/*
This is needed because, whenever we click the checkbox to enter data
manually, the checkbox will be focused causing that when we scan the
barcode afterwards, it will not perform the python on_barcode_scanned
function.
*/
onChange(newValue) {
super.onChange(newValue);
// We can't blur an element on its onchange event
// we need to wait for the event to finish (thus
// requestIdleCallback)
requestIdleCallback(() => {
document.activeElement.blur();
});
this.enableFormEdit(newValue);
}
enableFormEdit(newValue, editAction = false) {
// Enable edit form
if (this.props.name === "manual_entry" || editAction) {
const $form_edit = $("div.oe_stock_barcordes_content > div.scan_fields");
const $div_inventory_quant_ids = $("div[name='inventory_quant_ids']").find(
"div.o_kanban_renderer"
);
if ($form_edit.length > 0) {
if (newValue) {
$form_edit.removeClass("d-none");
$div_inventory_quant_ids.addClass("inventory_quant_ids_with_form");
$div_inventory_quant_ids.removeClass(
"inventory_quant_ids_without_form"
);
} else {
$form_edit.addClass("d-none");
$div_inventory_quant_ids.removeClass(
"inventory_quant_ids_with_form"
);
$div_inventory_quant_ids.addClass(
"inventory_quant_ids_without_form"
);
}
} else {
$div_inventory_quant_ids.addClass("inventory_quant_ids_without_form");
}
}
}
}
registry.category("fields").add("barcode_boolean_toggle", BarcodeBooleanToggleField);

View file

@ -0,0 +1,40 @@
/** @odoo-module */
/* Copyright 2022 Tecnativa - Alexandre D. Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
import {NumericStep} from "@web_widget_numeric_step/numeric_step.esm";
import {isAllowedBarcodeModel} from "../utils/barcodes_models_utils.esm";
import {patch} from "@web/core/utils/patch";
patch(NumericStep.prototype, "Adds barcode event handling and focus", {
_onFocus() {
if (isAllowedBarcodeModel(this.props.record.resModel)) {
// Auto select all content when user enters into fields with this
// widget.
this.inputRef.el.select();
}
},
_onKeyDown(ev) {
if (isAllowedBarcodeModel(this.props.record.resModel) && ev.keyCode === 13) {
const action_confirm = document.querySelector(
"button[name='action_confirm']"
);
if (action_confirm) {
action_confirm.click();
return;
}
const action_confirm_force = document.querySelector(
"button[name='action_force_done']"
);
if (action_confirm_force) {
action_confirm_force.click();
return;
}
}
this._super(...arguments);
},
});

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2024 Akretion
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<template>
<t
t-name="barcode_web_widget_numeric_step"
t-inherit="web_widget_numeric_step.web_widget_numeric_step"
t-inherit-mode="extension"
owl="1"
>
<xpath expr="//input" position="attributes">
<attribute name="t-on-focus">_onFocus</attribute>
</xpath>
</t>
</template>

View file

@ -0,0 +1,8 @@
/** @odoo-module */
import {ViewButton} from "@web/views/view_button/view_button";
import {patch} from "@web/core/utils/patch";
patch(ViewButton, "Add hotkey to button", {
props: [...ViewButton.props, "hotkey?"],
});

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="views.ViewButton"
t-inherit="web.views.ViewButton"
t-inherit-mode="extension"
owl="1"
>
<xpath expr="//t[@t-tag]" position="attributes">
<attribute name="t-att-data-hotkey">props.hotkey</attribute>
</xpath>
</t>
</templates>