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: 2.9 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,728 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="stock-barcodes">
<h1>Stock Barcodes</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e429e57aae9e2b85719c0aa8e1e85f19c26d6494f62b7ef84905992e263b043e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-barcode/tree/16.0/stock_barcodes"><img alt="OCA/stock-logistics-barcode" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--barcode-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-barcode-16-0/stock-logistics-barcode-16-0-stock_barcodes"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-barcode&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module provides a barcode reader interface for stock module.</p>
<p>This module contains a base wizard read barcode that can be extended by
other modules.</p>
<p>This module also makes use of this wizard for providing barcode support for
doing inventories and picking operations.</p>
<p>This module provides configuring barcodes for barcode actions.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a><ul>
<li><a class="reference internal" href="#barcode-interface-for-inventory-operations" id="toc-entry-2">Barcode interface for inventory operations</a></li>
<li><a class="reference internal" href="#barcode-interface-for-picking-operations" id="toc-entry-3">Barcode interface for picking operations</a></li>
<li><a class="reference internal" href="#automatic-operation-mode" id="toc-entry-4">Automatic operation mode</a></li>
<li><a class="reference internal" href="#manual-entry-mode" id="toc-entry-5">Manual entry mode</a></li>
<li><a class="reference internal" href="#scan-logs" id="toc-entry-6">Scan logs</a></li>
<li><a class="reference internal" href="#barcode-interface-for-barcode-actions" id="toc-entry-7">Barcode interface for barcode actions</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-8">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-9">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-10">11.0.1.1.0 (2019-09-24)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-11">13.0.1.1.1 (2021-02-06)</a></li>
<li><a class="reference internal" href="#section-3" id="toc-entry-12">14.0.1.0.0 (2021-04-05)</a></li>
<li><a class="reference internal" href="#section-4" id="toc-entry-13">16.0.1.0.0 (2025-01-23)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-14">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-15">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-16">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-17">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-18">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<div class="section" id="barcode-interface-for-inventory-operations">
<h3><a class="toc-backref" href="#toc-entry-2">Barcode interface for inventory operations</a></h3>
<p>Option 1: To use the barcode interface on inventory</p>
<blockquote>
<ol class="arabic simple">
<li>Go to <em>Inventory &gt; operations &gt; Inventory Adjustments</em>.</li>
<li>Create new inventory with “Select products manually” option.</li>
<li>Start inventory.</li>
<li>Click to “Scan barcodes” smart button.</li>
<li>Start reading barcodes.</li>
</ol>
</blockquote>
<dl class="docutils">
<dt>Option 2: Use the barcode interface inventory directly from the Barcodes application</dt>
<dd><ol class="first arabic simple">
<li>Go to <em>Barcodes</em>.</li>
<li>Select the <em>Inventory</em> option.</li>
</ol>
<blockquote>
<img alt="Inventory barcode action" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png" style="width: 200px; height: 100px;" />
</blockquote>
<ol class="last arabic simple">
<li>Start scanning barcodes.</li>
</ol>
</dd>
<dt>Actions</dt>
<dd><p class="first"># Press the <em>+ Product</em> button to display the form for the new item.</p>
<blockquote>
<img alt="Add product" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># When you select a product, a numeric field is displayed to add the quantity.</p>
<blockquote>
<img alt="Add quantity product" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.</p>
<blockquote>
<img alt="Reset data form" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># When you press the <em>Clean values</em> button, all fields are reset and the form is closed.
# When you press the <em>Confirm</em> button, the new item is added and the form is closed.
# When the eye icon is closed, the created items greater than zero are displayed, and if not, those less than or equal to zero.</p>
<blockquote>
<img alt="Reset data form" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># In the list, the trash can icon allows you to reset the quantity to zero and the edit icon allows you to change the item values.</p>
<blockquote>
<img alt="Reset data form" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_action_items.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># The <em>Apply</em> button is only displayed if there are items with quantities greater than zero, regardless of whether they were scanned or entered manually; If you press all the defined quantities will be processed after defining the reason for the inventory adjustment and then the main barcode menu will be displayed.</p>
<blockquote class="last">
<img alt="Apply inventory" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory.png" style="width: 200px; height: 100px;" />
<img alt="Apply inventory reason" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory_reason.png" style="width: 200px; height: 100px;" />
</blockquote>
</dd>
</dl>
</div>
<div class="section" id="barcode-interface-for-picking-operations">
<h3><a class="toc-backref" href="#toc-entry-3">Barcode interface for picking operations</a></h3>
<p>You can use the barcode interface in a picking or an operation type, the main
difference is that if you open the barcode interface from a picking, this
picking is locked and you read products for it.</p>
<p>To use the barcode interface on picking operations:</p>
<ol class="arabic simple">
<li>Go to <em>Inventory</em>.</li>
<li>Click on scanner button on any operation type.</li>
<li>Start reading barcodes.</li>
</ol>
<p>Option 1: To use the barcode interface on a picking:</p>
<blockquote>
<ol class="arabic simple">
<li>Go to <em>Inventory &gt; Transfers</em>.</li>
<li>Click to “Scan barcodes” smart button.</li>
<li>Start reading barcodes.</li>
</ol>
</blockquote>
<dl class="docutils">
<dt>Option 2: Use the barcode interface picking directly from the Barcodes application</dt>
<dd><ol class="first arabic simple">
<li>Go to <em>Barcodes</em>.</li>
<li>Select the option <em>OPERATIONS</em>.</li>
</ol>
<blockquote>
<img alt="Operation barcode action" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># Select the type of picking.
# The pickings in ready status are displayed, select the one you want to start scanning.</p>
<blockquote>
<img alt="List picking" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_picking.png" style="width: 200px; height: 100px;" />
</blockquote>
<ol class="arabic simple">
<li>Start scanning barcodes.</li>
</ol>
<blockquote class="last">
<img alt="List picking" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/barcode_interface_picking.png" style="width: 200px; height: 100px;" />
</blockquote>
</dd>
<dt>Actions</dt>
<dd><p class="first"># All the items that have been configured for the selected picking are listed.</p>
<blockquote>
<img alt="List picking" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># The edit icon in the list allows you to modify the data.</p>
<blockquote>
<img alt="Edit picking" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_edit.png" style="width: 200px; height: 100px;" />
</blockquote>
<dl class="docutils">
<dt># The button that contains a <em>+120</em> (in this case), allows you to define all the</dt>
<dd><p class="first">remaining quantities. Once defined, this button disappears and if you want to change the
quantities, press the edit button.</p>
<img alt="Quantity picking" class="last" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_quantity.png" style="width: 200px; height: 100px;" />
</dd>
<dt># If there is at least one item with a quantity already defined, an eye icon is displayed,</dt>
<dd><p class="first">which if closed shows the items and their quantities already scanned.</p>
<img alt="Picking scanned" class="last" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_scanned.png" style="width: 200px; height: 100px;" />
</dd>
<dt># When you press the <em>Validate</em> button, a wizard will be displayed to confirm the action.</dt>
<dd><p class="first">If everything is correct, it is validated and you return to the picking list mentioned above.</p>
<img alt="Picking scanned" class="last" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_items_picking.png" style="width: 200px; height: 100px;" />
</dd>
<dt># If there is an item whose quantity is zero, a wizard will be displayed after the one mentioned</dt>
<dd><p class="first">above, to confirm if you want to process all the quantities. If positive, you will proceed
and be directed to the list mentioned above in the previous point.</p>
<img alt="Picking scanned" class="last" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png" style="width: 200px; height: 100px;" />
</dd>
</dl>
<p># Press the <em>+ Product</em> button to display the form for the new item.</p>
<blockquote>
<img alt="Add product" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># When you select a product, a numeric field is displayed to add the quantity.</p>
<blockquote>
<img alt="Add quantity product" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png" style="width: 200px; height: 100px;" />
</blockquote>
<p># When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.</p>
<blockquote>
<img alt="Reset data form" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png" style="width: 200px; height: 100px;" />
</blockquote>
<p class="last"># When you press the <em>Clean values</em> button, all fields are reset and the form is closed.
# When you press the <em>Confirm</em> button, the new item is added and the form is closed.
# When adding the new item all the quantities are assigned to it, if you want to modify it, press the edit icon.</p>
</dd>
</dl>
<p>The barcode scanner interface has two operation modes. In both of them user
can scan:</p>
<ol class="arabic simple">
<li>Warehouse locations with barcode.</li>
<li>Product packaging with barcode.</li>
<li>Product with barcode.</li>
<li>Product Lots (The barcode is name field in this case).</li>
</ol>
</div>
<div class="section" id="automatic-operation-mode">
<h3><a class="toc-backref" href="#toc-entry-4">Automatic operation mode</a></h3>
<p>This is the default mode, all screen controls are locked to avoid scan into
fields.</p>
<p>The user only has to scan barcode in physical warehouse locations with a
scanner hardward, the interface read the barcode and do operations in this
order:</p>
<ol class="arabic simple">
<li>Try search a product, if found, is assigned to product_id field and creates
or update inventory line with 1.0 unit. (If product has tracking by lots
the interface wait for a lot to be scanned).</li>
<li>Try search a product packaging, if found, the product_id related is set,
product quantities are updated and create or update inventory line with
product quantities defined in the product packaging.</li>
<li>Try search a lot (The product is mandatory in this case so you first scan a
product and then scann a lot), this lot field is not erased until that
product change, so for each product scann the interface add or update a
inventory line with this lot.</li>
<li>Try to search a location, if found the field location is set and next scan
action will be done with this warehouse location.</li>
</ol>
<p>If barcode has not found, when message is displayed you can create this lot
scanning the product.</p>
</div>
<div class="section" id="manual-entry-mode">
<h3><a class="toc-backref" href="#toc-entry-5">Manual entry mode</a></h3>
<p>You can change to “manual entry” to allow to select data without scanner
hardware, but hardward scanner still active on, so a use case would be when
user wants set quantities manually instead increment 1.0 unit peer scan action.</p>
</div>
<div class="section" id="scan-logs">
<h3><a class="toc-backref" href="#toc-entry-6">Scan logs</a></h3>
<p>All scanned barcodes are saved into model.
Barcode scanning interface display 10 last records linked to model, the goal of
this log is show to user other reads with the same product and location done
by other users.
User can remove the last read scan.</p>
</div>
<div class="section" id="barcode-interface-for-barcode-actions">
<h3><a class="toc-backref" href="#toc-entry-7">Barcode interface for barcode actions</a></h3>
<p>To use the barcode interface for actions:</p>
<ol class="arabic simple">
<li>Go to <em>Inventory &gt; Configuration &gt; Barcode Actions</em>.</li>
<li>Create a new barcode action and configure the barcode.</li>
</ol>
<img alt="Print barcodes" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/create_barcode_action.png" style="width: 200px; height: 100px;" />
<ol class="arabic simple">
<li>Select the barcode actions you want to use, a button (PRINT BARCODES) will appear that allows you to print the configured barcodes to PDF.</li>
</ol>
<img alt="Print barcodes" src="https://raw.githubusercontent.com/stock_barcodes/static/src/img/print_barcodes.png" style="width: 200px; height: 100px;" />
<ol class="arabic simple">
<li>Go to <em>Barcodes</em>.</li>
<li>Start scanning barcodes from actions.</li>
</ol>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#toc-entry-8">Known issues / Roadmap</a></h2>
<ul class="simple">
<li>Excute action_done() method outside onchange environment.</li>
<li>Allow create product when a barcode has not been found.</li>
<li>Allow to select picking reading its barcode.</li>
<li>Allow to select multiple pickings to process scanned products.</li>
</ul>
</div>
<div class="section" id="changelog">
<h2><a class="toc-backref" href="#toc-entry-9">Changelog</a></h2>
<div class="section" id="section-1">
<h3><a class="toc-backref" href="#toc-entry-10">11.0.1.1.0 (2019-09-24)</a></h3>
<ul class="simple">
<li>[ADD] New feature.
User can uses barcode interface in picking operations.</li>
</ul>
</div>
<div class="section" id="section-2">
<h3><a class="toc-backref" href="#toc-entry-11">13.0.1.1.1 (2021-02-06)</a></h3>
<ul class="simple">
<li>[ADD] New feature.
Add option to get lots automatically based on removal strategy in inventory.</li>
</ul>
</div>
<div class="section" id="section-3">
<h3><a class="toc-backref" href="#toc-entry-12">14.0.1.0.0 (2021-04-05)</a></h3>
<ul class="simple">
<li>[ADD] New feature.
Add security for users.</li>
</ul>
</div>
<div class="section" id="section-4">
<h3><a class="toc-backref" href="#toc-entry-13">16.0.1.0.0 (2025-01-23)</a></h3>
<ul class="simple">
<li>[IMP]
Improved views to optimize navigation and functionality.
Intuitive and mobile-friendly views.
Visual improvement of the main view accessed from the Barcodes menu.</li>
<li>[ADD] New feature.
Barcode reading to barcode actions.
Generate PDF document for the barcodes of the selected barcode actions.</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-14">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/issues/new?body=module:%20stock_barcodes%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-15">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-16">Authors</a></h3>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-17">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Sergio Teruel</li>
<li>Carlos Dauden</li>
<li>Pedro M. Baeza</li>
<li>Alexandre D. Díaz</li>
</ul>
</li>
<li><a class="reference external" href="https://www.onestein.eu">Onestein</a>:<ul>
<li>Andrea Stirpe</li>
</ul>
</li>
<li><a class="reference external" href="https://www.initos.com">InitOS</a>:<ul>
<li>Foram Shah</li>
</ul>
</li>
<li><a class="reference external" href="https://www.forgeflow.com">ForgeFlow</a>:<ul>
<li>Lois Rilo</li>
</ul>
</li>
<li>Enric Tobella</li>
<li><a class="reference external" href="https://www.binhex.cloud/">Binhex Cloud</a>:<ul>
<li>Edilio Escalona Almira</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-18">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/tree/16.0/stock_barcodes">OCA/stock-logistics-barcode</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

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>