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

View file

@ -0,0 +1,50 @@
# Stock Barcodes
Odoo addon: stock_barcodes
## Installation
```bash
pip install odoo-bringout-oca-stock-logistics-barcode-stock_barcodes
```
## Dependencies
This addon depends on:
- barcodes
- stock
- web_widget_numeric_step
- web
- mail
## Manifest Information
- **Name**: Stock Barcodes
- **Version**: 16.0.2.0.0
- **Category**: Extra Tools
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/stock-logistics-barcode](https://github.com/OCA/stock-logistics-barcode) branch 16.0, addon `stock_barcodes`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Stock_barcodes Module - stock_barcodes
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for stock_barcodes. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,9 @@
# Dependencies
This addon depends on:
- [barcodes](../../odoo-bringout-oca-ocb-barcodes)
- [stock](../../odoo-bringout-oca-ocb-stock)
- [web_widget_numeric_step](../../odoo-bringout-oca-web-web_widget_numeric_step)
- [web](../../odoo-bringout-oca-ocb-web)
- [mail](../../odoo-bringout-oca-ocb-mail)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon stock_barcodes or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-stock-logistics-barcode-stock_barcodes"
# or
uv pip install odoo-bringout-oca-stock-logistics-barcode-stock_barcodes"
```

View file

@ -0,0 +1,20 @@
# Models
Detected core models and extensions in stock_barcodes.
```mermaid
classDiagram
class stock_barcodes_action
class stock_barcodes_option
class stock_barcodes_option_group
class stock_quant
class barcodes_barcode_events_mixin
class stock_move
class stock_move_line
class stock_picking
class stock_picking_type
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: stock_barcodes. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon stock_barcodes
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,34 @@
# Security
Access control and security definitions in stock_barcodes.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../stock_barcodes/security/ir.model.access.csv)**
- 10 model access rules
## Record Rules
Row-level security rules defined in:
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[ir.model.access.csv](../stock_barcodes/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon stock_barcodes
```

View file

@ -0,0 +1,12 @@
# Wizards
Transient models exposed as UI wizards in stock_barcodes.
```mermaid
classDiagram
class WizCandidatePicking
class WizStockBarcodesNewLot
class WizStockBarcodesReadInventory
class WizStockBarcodesReadPicking
class WizStockBarcodesReadTodo
```

View file

@ -0,0 +1,46 @@
[project]
name = "odoo-bringout-oca-stock-logistics-barcode-stock_barcodes"
version = "16.0.0"
description = "Stock Barcodes - It provides read barcode on stock operations."
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-stock-logistics-barcode-barcodes>=16.0.0",
"odoo-bringout-oca-ocb-stock>=16.0.0",
"odoo-bringout-oca-stock-logistics-barcode-web_widget_numeric_step>=16.0.0",
"odoo-bringout-oca-ocb-web>=16.0.0",
"odoo-bringout-oca-ocb-mail>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["stock_barcodes"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,419 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
==============
Stock Barcodes
==============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e429e57aae9e2b85719c0aa8e1e85f19c26d6494f62b7ef84905992e263b043e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--barcode-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-barcode/tree/16.0/stock_barcodes
:alt: OCA/stock-logistics-barcode
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-barcode-16-0/stock-logistics-barcode-16-0-stock_barcodes
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-barcode&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module provides a barcode reader interface for stock module.
This module contains a base wizard read barcode that can be extended by
other modules.
This module also makes use of this wizard for providing barcode support for
doing inventories and picking operations.
This module provides configuring barcodes for barcode actions.
**Table of contents**
.. contents::
:local:
Usage
=====
Barcode interface for inventory operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Option 1: To use the barcode interface on inventory
#. Go to *Inventory > operations > Inventory Adjustments*.
#. Create new inventory with "Select products manually" option.
#. Start inventory.
#. Click to "Scan barcodes" smart button.
#. Start reading barcodes.
Option 2: Use the barcode interface inventory directly from the Barcodes application
#. Go to *Barcodes*.
#. Select the *Inventory* option.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png
:height: 100
:width: 200
:alt: Inventory barcode action
#. Start scanning barcodes.
Actions
# Press the *+ Product* button to display the form for the new item.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png
:height: 100
:width: 200
:alt: Add product
# When you select a product, a numeric field is displayed to add the quantity.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png
:height: 100
:width: 200
:alt: Add quantity product
# When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png
:height: 100
:width: 200
:alt: Reset data form
# When you press the *Clean values* button, all fields are reset and the form is closed.
# When you press the *Confirm* 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.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items.png
:height: 100
:width: 200
:alt: Reset data form
# 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.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_action_items.png
:height: 100
:width: 200
:alt: Reset data form
# The *Apply* 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.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory.png
:height: 100
:width: 200
:alt: Apply inventory
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/apply_inventory_reason.png
:height: 100
:width: 200
:alt: Apply inventory reason
Barcode interface for picking operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
To use the barcode interface on picking operations:
#. Go to *Inventory*.
#. Click on scanner button on any operation type.
#. Start reading barcodes.
Option 1: To use the barcode interface on a picking:
#. Go to *Inventory > Transfers*.
#. Click to "Scan barcodes" smart button.
#. Start reading barcodes.
Option 2: Use the barcode interface picking directly from the Barcodes application
#. Go to *Barcodes*.
#. Select the option *OPERATIONS*.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/inventory_barcode_action.png
:height: 100
:width: 200
:alt: Operation barcode action
# Select the type of picking.
# The pickings in ready status are displayed, select the one you want to start scanning.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_picking.png
:height: 100
:width: 200
:alt: List picking
#. Start scanning barcodes.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/barcode_interface_picking.png
:height: 100
:width: 200
:alt: List picking
Actions
# All the items that have been configured for the selected picking are listed.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking.png
:height: 100
:width: 200
:alt: List picking
# The edit icon in the list allows you to modify the data.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_edit.png
:height: 100
:width: 200
:alt: Edit picking
# The button that contains a *+120* (in this case), allows you to define all the
remaining quantities. Once defined, this button disappears and if you want to change the
quantities, press the edit button.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_quantity.png
:height: 100
:width: 200
:alt: Quantity picking
# If there is at least one item with a quantity already defined, an eye icon is displayed,
which if closed shows the items and their quantities already scanned.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/list_items_picking_scanned.png
:height: 100
:width: 200
:alt: Picking scanned
# When you press the *Validate* button, a wizard will be displayed to confirm the action.
If everything is correct, it is validated and you return to the picking list mentioned above.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_items_picking.png
:height: 100
:width: 200
:alt: Picking scanned
# If there is an item whose quantity is zero, a wizard will be displayed after the one mentioned
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.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png
:height: 100
:width: 200
:alt: Picking scanned
# Press the *+ Product* button to display the form for the new item.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/add_product.png
:height: 100
:width: 200
:alt: Add product
# When you select a product, a numeric field is displayed to add the quantity.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_quantity.png
:height: 100
:width: 200
:alt: Add quantity product
# When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/form_add_product_reset.png
:height: 100
:width: 200
:alt: Reset data form
# When you press the *Clean values* button, all fields are reset and the form is closed.
# When you press the *Confirm* 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.
The barcode scanner interface has two operation modes. In both of them user
can scan:
#. Warehouse locations with barcode.
#. Product packaging with barcode.
#. Product with barcode.
#. Product Lots (The barcode is name field in this case).
Automatic operation mode
~~~~~~~~~~~~~~~~~~~~~~~~
This is the default mode, all screen controls are locked to avoid scan into
fields.
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:
#. 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).
#. 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.
#. 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.
#. Try to search a location, if found the field location is set and next scan
action will be done with this warehouse location.
If barcode has not found, when message is displayed you can create this lot
scanning the product.
Manual entry mode
~~~~~~~~~~~~~~~~~
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.
Scan logs
~~~~~~~~~
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.
Barcode interface for barcode actions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use the barcode interface for actions:
#. Go to *Inventory > Configuration > Barcode Actions*.
#. Create a new barcode action and configure the barcode.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/create_barcode_action.png
:height: 100
:width: 200
:alt: Print barcodes
#. Select the barcode actions you want to use, a button (PRINT BARCODES) will appear that allows you to print the configured barcodes to PDF.
.. image:: https://raw.githubusercontent.com/stock_barcodes/static/src/img/print_barcodes.png
:height: 100
:width: 200
:alt: Print barcodes
#. Go to *Barcodes*.
#. Start scanning barcodes from actions.
Known issues / Roadmap
======================
* Excute action_done() method outside onchange environment.
* Allow create product when a barcode has not been found.
* Allow to select picking reading its barcode.
* Allow to select multiple pickings to process scanned products.
Changelog
=========
11.0.1.1.0 (2019-09-24)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
User can uses barcode interface in picking operations.
13.0.1.1.1 (2021-02-06)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
Add option to get lots automatically based on removal strategy in inventory.
14.0.1.0.0 (2021-04-05)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
Add security for users.
16.0.1.0.0 (2025-01-23)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP]
Improved views to optimize navigation and functionality.
Intuitive and mobile-friendly views.
Visual improvement of the main view accessed from the Barcodes menu.
* [ADD] New feature.
Barcode reading to barcode actions.
Generate PDF document for the barcodes of the selected barcode actions.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-barcode/issues>`_.
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
`feedback <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**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`_:
* Sergio Teruel
* Carlos Dauden
* Pedro M. Baeza
* Alexandre D. Díaz
* `Onestein <https://www.onestein.eu>`_:
* Andrea Stirpe
* `InitOS <https://www.initos.com>`_:
* Foram Shah
* `ForgeFlow <https://www.forgeflow.com>`_:
* Lois Rilo
* Enric Tobella
* `Binhex Cloud <https://www.binhex.cloud/>`_:
* Edilio Escalona Almira
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
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.
This module is part of the `OCA/stock-logistics-barcode <https://github.com/OCA/stock-logistics-barcode/tree/16.0/stock_barcodes>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,7 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models
from . import wizard
from . import reports
from .hooks import pre_init_hook

View file

@ -0,0 +1,47 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Stock Barcodes",
"summary": "It provides read barcode on stock operations.",
"version": "16.0.2.0.0",
"author": "Tecnativa, " "Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-barcode",
"license": "AGPL-3",
"category": "Extra Tools",
"depends": ["barcodes", "stock", "web_widget_numeric_step", "web", "mail"],
"data": [
"security/ir.model.access.csv",
"views/stock_barcodes_action_view.xml",
"views/stock_barcodes_option_view.xml",
"views/stock_location_views.xml",
"views/stock_picking_views.xml",
"wizard/stock_production_lot_views.xml",
"wizard/stock_barcodes_read_views.xml",
"wizard/stock_barcodes_read_picking_views.xml",
"wizard/stock_barcodes_read_todo_view.xml",
"wizard/stock_barcodes_read_inventory_views.xml",
# Keep order
"data/stock_barcodes_action.xml",
"data/stock_barcodes_option.xml",
"views/stock_barcodes_menu.xml",
# Reports
"reports/barcode_actions_report.xml",
"reports/reports.xml",
],
"assets": {
"web.assets_backend": [
"/stock_barcodes/static/src/**/*.esm.js",
(
"after",
"/web_widget_numeric_step/static/src/numeric_step.xml",
"/stock_barcodes/static/src/widgets/numeric_step.xml",
),
"/stock_barcodes/static/src/views/kanban/stock_barcodes_kanban.xml",
"/stock_barcodes/static/src/widgets/view_button.xml",
"/stock_barcodes/static/src/views/actions/stock_barcode_main_menu.xml",
"/stock_barcodes/static/src/**/*.scss",
],
},
"installable": True,
"pre_init_hook": "pre_init_hook",
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="stock_barcodes_action_picking_in" model="stock.barcodes.action">
<field name="name">Picking IN</field>
<field name="sequence">10</field>
<field name="action_window_id" ref="stock.stock_picking_type_action" />
<field name="key_char_shortcut">1</field>
<field
name="context"
>{'search_default_code': 'incoming', 'search_default_barcode_options': 1}</field>
</record>
<record id="stock_barcodes_action_picking_int" model="stock.barcodes.action">
<field name="name">Picking INTERNAL</field>
<field name="sequence">30</field>
<field name="action_window_id" ref="stock.stock_picking_type_action" />
<field name="key_char_shortcut">3</field>
<field
name="context"
>{'search_default_code': 'internal', 'search_default_barcode_options': 1}</field>
</record>
<record id="stock_barcodes_action_picking_out" model="stock.barcodes.action">
<field name="name">Picking OUT</field>
<field name="sequence">20</field>
<field name="action_window_id" ref="stock.stock_picking_type_action" />
<field name="key_char_shortcut">2</field>
<field
name="context"
>{'search_default_code': 'outgoing', 'search_default_barcode_options': 1}</field>
</record>
<!-- Action for inventory based on quants -->
<record id="stock_barcodes_action_inventory" model="stock.barcodes.action">
<field name="name">Inventory</field>
<field name="sequence">40</field>
<field
name="action_window_id"
ref="stock_barcodes.action_stock_barcodes_read_inventory"
/>
<field name="key_char_shortcut">8</field>
<field name="context">{'inventory_mode': True}</field>
</record>
<record id="stock_barcodes_action_barcode_operations" model="stock.barcodes.action">
<field name="name">Operations</field>
<field name="sequence">40</field>
<field name="action_window_id" ref="stock.stock_picking_type_action" />
<field name="key_char_shortcut">9</field>
<field name="context">{'operations_mode': True}</field>
</record>
</odoo>

View file

@ -0,0 +1,635 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record
id="stock_barcodes_option_group_picking_out"
model="stock.barcodes.option.group"
>
<field name="name">Picking OUT options</field>
<field name="code">OUT</field>
<field name="is_manual_qty">True</field>
<field name="is_manual_confirm">True</field>
<field name="barcode_guided_mode">guided</field>
<field name="show_pending_moves">True</field>
<field name="confirmed_moves">True</field>
<field name="source_pending_moves">move_ids</field>
<field name="fill_fields_from_lot">True</field>
</record>
<record
id="stock_barcodes_option_package_picking_out"
model="stock.barcodes.option"
>
<field name="name">Package</field>
<field name="step">1</field>
<field name="sequence">10</field>
<field name="field_name">package_id</field>
<field name="to_scan">True</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">False</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_out"
/>
</record>
<record
id="stock_barcodes_option_product_picking_out"
model="stock.barcodes.option"
>
<field name="name">Product</field>
<field name="step">1</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">True</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_out"
/>
</record>
<record id="stock_barcodes_option_lot_picking_out" model="stock.barcodes.option">
<field name="name">Lot</field>
<field name="step">1</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_out"
/>
</record>
<record
id="stock_barcodes_option_location_picking_out"
model="stock.barcodes.option"
>
<field name="name">Source</field>
<field name="step">2</field>
<field name="sequence">40</field>
<field name="field_name">location_id</field>
<field name="to_scan">False</field>
<field name="filled_default">True</field>
<field name="forced">True</field>
<field name="clean_after_done">False</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_out"
/>
</record>
<record
id="stock_barcodes_option_product_qty_picking_out"
model="stock.barcodes.option"
>
<field name="name">Qty.</field>
<field name="step">3</field>
<field name="sequence">50</field>
<field name="field_name">product_qty</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_out"
/>
</record>
<!-- Incoming options -->
<record
id="stock_barcodes_option_group_picking_in"
model="stock.barcodes.option.group"
>
<field name="name">Picking IN options</field>
<field name="code">IN</field>
<field name="auto_put_in_pack">True</field>
<field name="is_manual_qty">False</field>
<field name="is_manual_confirm">False</field>
<field name="barcode_guided_mode" />
<field name="show_pending_moves">True</field>
<field name="confirmed_moves">False</field>
<field name="source_pending_moves">move_line_ids</field>
<field name="fill_fields_from_lot">False</field>
</record>
<record
id="stock_barcodes_option_location_dest_picking_in"
model="stock.barcodes.option"
>
<field name="name">Dest.</field>
<field name="step">1</field>
<field name="sequence">10</field>
<field name="field_name">location_dest_id</field>
<field name="to_scan">True</field>
<field name="filled_default">True</field>
<field name="forced">False</field>
<field name="clean_after_done">False</field>
<field name="required">False</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_in"
/>
</record>
<record id="stock_barcodes_option_product_picking_in" model="stock.barcodes.option">
<field name="name">Product</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="filled_default">True</field>
<field name="forced">True</field>
<field name="to_scan">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_in"
/>
</record>
<record id="stock_barcodes_option_lot_picking_in" model="stock.barcodes.option">
<field name="name">Lot</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="filled_default">True</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_in"
/>
</record>
<record
id="stock_barcodes_option_product_qty_picking_in"
model="stock.barcodes.option"
>
<field name="name">Product Qty</field>
<field name="sequence">40</field>
<field name="field_name">product_qty</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_in"
/>
</record>
<!-- Internal transfers options -->
<record
id="stock_barcodes_option_group_picking_internal"
model="stock.barcodes.option.group"
>
<field name="name">Picking Internal options</field>
<field name="code">INT</field>
<field name="barcode_guided_mode">guided</field>
<field name="auto_put_in_pack">False</field>
<field name="is_manual_qty">False</field>
<field name="is_manual_confirm">True</field>
<field name="show_pending_moves">True</field>
<field name="confirmed_moves">True</field>
<field name="source_pending_moves">move_line_ids</field>
<field name="fill_fields_from_lot">True</field>
</record>
<record
id="stock_barcodes_option_location_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Source</field>
<field name="step">1</field>
<field name="sequence">10</field>
<field name="field_name">location_id</field>
<field name="to_scan">False</field>
<field name="filled_default">True</field>
<field name="forced">True</field>
<field name="clean_after_done">False</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<record
id="stock_barcodes_option_product_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Prod.</field>
<field name="step">2</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">True</field>
<field name="clean_after_done">False</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<record
id="stock_barcodes_option_package_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Package</field>
<field name="step">2</field>
<field name="sequence">30</field>
<field name="field_name">package_id</field>
<field name="to_scan">True</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">False</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<record
id="stock_barcodes_option_lot_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Lot</field>
<field name="step">2</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<record
id="stock_barcodes_option_location_dest_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Dest.</field>
<field name="step">3</field>
<field name="sequence">50</field>
<field name="field_name">location_dest_id</field>
<field name="to_scan">True</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<record
id="stock_barcodes_option_product_qty_picking_internal"
model="stock.barcodes.option"
>
<field name="name">Qty</field>
<field name="step">4</field>
<field name="sequence">60</field>
<field name="field_name">product_qty</field>
<field name="to_scan">False</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="clean_after_done">True</field>
<field name="required">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_internal"
/>
</record>
<!-- Relocation options -->
<record
id="stock_barcodes_option_group_picking_relocation"
model="stock.barcodes.option.group"
>
<field name="name">Picking relocation options</field>
<field name="code">REL</field>
<field name="barcode_guided_mode" />
<field name="auto_put_in_pack">False</field>
<field name="is_manual_qty">False</field>
<field name="is_manual_confirm">True</field>
<field name="ignore_filled_fields">True</field>
<field name="show_pending_moves">False</field>
<field name="confirmed_moves">False</field>
<field name="source_pending_moves">move_line_ids</field>
<field name="fill_fields_from_lot">True</field>
</record>
<record
id="stock_barcodes_option_location_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Source</field>
<field name="step">0</field>
<field name="sequence">10</field>
<field name="field_name">location_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_product_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Product</field>
<field name="step">1</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_package_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Package</field>
<field name="step">1</field>
<field name="sequence">25</field>
<field name="field_name">package_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_lot_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Lot</field>
<field name="step">1</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_result_package_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Package Dest.</field>
<field name="step">2</field>
<field name="sequence">35</field>
<field name="field_name">result_package_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_location_dest_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Dest.</field>
<field name="step">2</field>
<field name="sequence">40</field>
<field name="field_name">location_dest_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<record
id="stock_barcodes_option_product_qty_picking_relocation"
model="stock.barcodes.option"
>
<field name="name">Product Qty</field>
<field name="step">3</field>
<field name="sequence">50</field>
<field name="field_name">product_qty</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_picking_relocation"
/>
</record>
<!-- Option group for Inventories -->
<record
id="stock_barcodes_option_group_inventory"
model="stock.barcodes.option.group"
>
<field name="name">Inventory options</field>
<field name="code">INV</field>
<field name="barcode_guided_mode" />
</record>
<record
id="stock_barcodes_option_location_id_inventory"
model="stock.barcodes.option"
>
<field name="name">Location</field>
<field name="step">1</field>
<field name="sequence">10</field>
<field name="field_name">location_id</field>
<field name="filled_default">True</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">False</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_inventory"
/>
</record>
<record
id="stock_barcodes_option_packaging_id_inventory"
model="stock.barcodes.option"
>
<field name="name">Packaging</field>
<field name="step">2</field>
<field name="sequence">10</field>
<field name="field_name">packaging_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">False</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_inventory"
/>
</record>
<record
id="stock_barcodes_option_product_id_inventory"
model="stock.barcodes.option"
>
<field name="name">Product</field>
<field name="step">2</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_inventory"
/>
</record>
<record id="stock_barcodes_option_lot_id_inventory" model="stock.barcodes.option">
<field name="name">Lot</field>
<field name="step">2</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_inventory"
/>
</record>
<record
id="stock_barcodes_option_product_qty_inventory"
model="stock.barcodes.option"
>
<field name="name">Product Qty</field>
<field name="step">3</field>
<field name="sequence">50</field>
<field name="field_name">product_qty</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_inventory"
/>
</record>
<!-- Option group for Operations -->
<record
id="stock_barcodes_option_group_operation"
model="stock.barcodes.option.group"
>
<field name="name">Operation options</field>
<field name="code">OPE</field>
<field name="barcode_guided_mode" />
</record>
<record
id="stock_barcodes_option_location_id_operation"
model="stock.barcodes.option"
>
<field name="name">Location</field>
<field name="step">1</field>
<field name="sequence">10</field>
<field name="field_name">location_id</field>
<field name="filled_default">True</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">False</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_operation"
/>
</record>
<record
id="stock_barcodes_option_packaging_id_operation"
model="stock.barcodes.option"
>
<field name="name">Packaging</field>
<field name="step">2</field>
<field name="sequence">10</field>
<field name="field_name">packaging_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">False</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_operation"
/>
</record>
<record
id="stock_barcodes_option_product_id_operation"
model="stock.barcodes.option"
>
<field name="name">Product</field>
<field name="step">2</field>
<field name="sequence">20</field>
<field name="field_name">product_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_operation"
/>
</record>
<record id="stock_barcodes_option_lot_id_operation" model="stock.barcodes.option">
<field name="name">Lot</field>
<field name="step">2</field>
<field name="sequence">30</field>
<field name="field_name">lot_id</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">True</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_operation"
/>
</record>
<record
id="stock_barcodes_option_product_qty_operation"
model="stock.barcodes.option"
>
<field name="name">Product Qty</field>
<field name="step">3</field>
<field name="sequence">50</field>
<field name="field_name">product_qty</field>
<field name="filled_default">False</field>
<field name="forced">False</field>
<field name="to_scan">False</field>
<field name="required">True</field>
<field name="clean_after_done">True</field>
<field
name="option_group_id"
ref="stock_barcodes.stock_barcodes_option_group_operation"
/>
</record>
<!-- End operation options -->
</odoo>

View file

@ -0,0 +1,20 @@
# Copyright 2021 Tecnativa - Sergio Teruel
# Copyright 2024 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
def pre_init_hook(cr):
cr.execute(
"""
ALTER TABLE stock_move_line
ADD COLUMN IF NOT EXISTS barcode_scan_state VARCHAR DEFAULT 'pending';
ALTER TABLE stock_move_line ALTER COLUMN barcode_scan_state DROP DEFAULT;
""",
)
cr.execute(
"""
ALTER TABLE stock_move
ADD COLUMN IF NOT EXISTS barcode_backorder_action VARCHAR DEFAULT 'pending';
ALTER TABLE stock_move ALTER COLUMN barcode_backorder_action DROP DEFAULT;
""",
)

View file

@ -0,0 +1,8 @@
from . import stock_barcodes_action
from . import stock_barcodes_option
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import stock_picking_type
from . import stock_quant
from . import barcode_events_mixin

View file

@ -0,0 +1,11 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class BarcodesEventsMixin(models.AbstractModel):
_inherit = "barcodes.barcode_events_mixin"
def send_bus_done(self, channel, type_channel, data=None):
self.env["bus.bus"]._sendone(channel, type_channel, data or {})

View file

@ -0,0 +1,189 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
import re
from io import BytesIO
import barcode
from barcode.writer import ImageWriter
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
REGEX = {
"context": r"^[^\s].*[^\s]$|^$",
"barcode": "^[a-zA-Z0-9-]+$",
}
FIELDS_NAME = {"barcode_options": "barcode_option_group_id"}
class StockBarcodesAction(models.Model):
_name = "stock.barcodes.action"
_description = "Actions for barcode interface"
_order = "sequence, id"
name = fields.Char(translate=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=100)
action_window_id = fields.Many2one(
comodel_name="ir.actions.act_window", string="Action window"
)
context = fields.Char()
key_shortcut = fields.Integer()
key_char_shortcut = fields.Char()
icon_class = fields.Char()
barcode = fields.Char()
barcode_image = fields.Image(
"Barcode image",
readonly=True,
compute="_compute_barcode_image",
attachment=True,
)
count_elements = fields.Integer(default=0, compute="_compute_count_elements")
@api.constrains("barcode")
def _constrains_barcode(self):
for action in self:
if not re.match(REGEX.get("barcode", False), action.barcode):
raise ValidationError(
_(
" The barcode {} is not correct."
"Use numbers, letters and dashes, without spaces."
"E.g. 15753, BC-5789,er-56 "
""
).format(action.barcode)
)
all_barcode = [bar for bar in action.mapped("barcode") if bar]
domain = [("barcode", "in", all_barcode)]
matched_actions = self.sudo().search(domain, order="id")
if len(matched_actions) > len(all_barcode):
raise ValidationError(
_(
""" Barcode has already been assigned to the action(s): {}."""
).format(", ".join(matched_actions.mapped("name")))
)
def _generate_barcode(self):
barcode_type = barcode.get_barcode_class("code128")
buffer = BytesIO()
barcode_instance = barcode_type(self.barcode, writer=ImageWriter())
barcode_instance.write(buffer)
buffer.seek(0)
image_base64 = base64.b64encode(buffer.getvalue())
return image_base64
@api.depends("barcode")
def _compute_barcode_image(self):
for action in self:
if action.barcode:
action.barcode_image = action._generate_barcode()
else:
action.barcode_image = False
@api.constrains("context")
def _constrains_context(self):
if self.context and not bool(
re.match(REGEX.get("context", False), self.context)
):
raise ValidationError(_("There can be no spaces at the beginning or end."))
def _count_elements(self):
domain = []
if self.context:
context_values = self.context.strip("{}").split(",")
def _map_context_values(x):
field_values = x.split(":")
field_name = field_values[0].split("search_default_")
if len(field_name) > 1:
field_name = field_name[1].strip("'")
field_value_format = field_values[1].replace("'", "").strip()
field_value = (
int(field_value_format)
if field_value_format.isdigit()
else field_value_format
)
if hasattr(
self.action_window_id.res_model,
FIELDS_NAME.get(field_name, field_name),
):
return (
"{}".format(FIELDS_NAME.get(field_name, field_name)),
"=",
field_value,
)
else:
return False
else:
return ()
domain = [
val_domain
for val_domain in list(
map(lambda x: _map_context_values(x), context_values)
)
]
search_count = (
list(filter(lambda x: x, domain))
if all(val_d is True for val_d in domain)
else []
)
return (
self.env[self.action_window_id.res_model].search_count(search_count)
if self.action_window_id.res_model
else 0
)
return 0
@api.depends("context")
def _compute_count_elements(self):
for barcode_action in self:
barcode_action.count_elements = (
barcode_action._count_elements()
if "search_default_" in barcode_action.context
else 0
)
def open_action(self):
action = self.action_window_id.sudo().read()[0]
action_context = safe_eval(action["context"])
ctx = self.env.context.copy()
if action_context:
ctx.update(action_context)
if self.context:
ctx.update(safe_eval(self.context))
if action_context.get("inventory_mode", False):
action = self.open_inventory_action(ctx)
else:
action["context"] = ctx
return action
def open_inventory_action(self, ctx):
option_group = self.env.ref(
"stock_barcodes.stock_barcodes_option_group_inventory"
)
vals = {
"option_group_id": option_group.id,
"manual_entry": option_group.manual_entry,
"display_read_quant": option_group.display_read_quant,
}
if option_group.get_option_value("location_id", "filled_default"):
vals["location_id"] = (
self.env["stock.warehouse"].search([], limit=1).lot_stock_id.id
)
wiz = self.env["wiz.stock.barcodes.read.inventory"].create(vals)
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_inventory"
)
action["res_id"] = wiz.id
action["context"] = ctx
return action
def print_barcodes(self):
report_action = self.env.ref(
"stock_barcodes.action_report_barcode_actions"
).report_action(None, data={})
return report_action

View file

@ -0,0 +1,127 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class StockBarcodesOptionGroup(models.Model):
_name = "stock.barcodes.option.group"
_description = "Options group for barcode interface"
name = fields.Char()
code = fields.Char()
option_ids = fields.One2many(
comodel_name="stock.barcodes.option", inverse_name="option_group_id", copy=True
)
barcode_guided_mode = fields.Selection(
[("guided", "Guided")],
string="Mode",
help="When guided mode is selected, information will appear with the "
"movement to be processed",
)
manual_entry = fields.Boolean(
string="Manual entry",
help="Default value when open scan interface",
)
manual_entry_field_focus = fields.Char(
help="Set field to set focus when manual entry mode is enabled",
default="location_id",
)
confirmed_moves = fields.Boolean(
string="Confirmed moves",
help="It allows to work with movements without reservation "
"(Without detailed operations)",
)
show_pending_moves = fields.Boolean(
string="Show pending moves", help="Shows a list of movements to process"
)
source_pending_moves = fields.Selection(
[("move_line_ids", "Detailed operations"), ("move_ids", "Operations")],
default="move_line_ids",
help="Origin of the data to generate the movements to process",
)
ignore_filled_fields = fields.Boolean(
string="Ignore filled fields",
)
auto_put_in_pack = fields.Boolean(
string="Auto put in pack", help="Auto put in pack before picking validation"
)
is_manual_qty = fields.Boolean(
help="If it is checked, it always shows the product quantity field in edit mode"
)
is_manual_confirm = fields.Boolean(
help="If it is marked, the movement must always be confirmed from a button"
)
allow_negative_quant = fields.Boolean(
help="If it is checked, it will allow the creation of movements that "
"generate negative stock"
)
fill_fields_from_lot = fields.Boolean(
help="If checked, the fields in the interface will be filled from "
"the scanned lot"
)
ignore_quant_location = fields.Boolean(
help="If it is checked, quant location will be ignored when reading lot/package",
)
group_key_for_todo_records = fields.Char(
help="You can establish a list of fields that will act as a grouping "
"key to generate the movements to be process.\n"
"The object variable is used to refer to the source record\n"
"For example, object.location_id,object.product_id,object.lot_id"
)
auto_lot = fields.Boolean(
string="Get lots automatically",
help="If checked the lot will be set automatically with the same "
"removal startegy",
)
create_lot = fields.Boolean(
string="Create lots if not match",
help="If checked the lot will created automatically with the scanned barcode "
"if not exists ",
)
show_detailed_operations = fields.Boolean(
help="If checked the picking detailed operations are displayed",
)
keep_screen_values = fields.Boolean(
help="If checked the wizard values are kept until the pending move is completed",
)
accumulate_read_quantity = fields.Boolean(
help="If checked quantity will be accumulated to the existing record instead of "
"overwrite it with the new quantity value",
)
display_notification = fields.Boolean(
string="Display Odoo notifications",
)
use_location_dest_putaway = fields.Boolean(
string="Use location dest. putaway",
)
location_field_to_sort = fields.Selection(
selection=[
("location_id", "Origin Location"),
("location_dest_id", "Destination Location"),
]
)
display_read_quant = fields.Boolean(string="Read items on inventory mode")
def get_option_value(self, field_name, attribute):
option = self.option_ids.filtered(lambda op: op.field_name == field_name)[:1]
return option[attribute]
class StockBarcodesOption(models.Model):
_name = "stock.barcodes.option"
_description = "Options for barcode interface"
_order = "step, sequence, id"
sequence = fields.Integer(default=100)
name = fields.Char()
option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group", ondelete="cascade"
)
field_name = fields.Char()
filled_default = fields.Boolean()
forced = fields.Boolean()
to_scan = fields.Boolean()
required = fields.Boolean()
clean_after_done = fields.Boolean()
message = fields.Char()
step = fields.Integer()

View file

@ -0,0 +1,37 @@
# Copyright 2024 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class StockMove(models.Model):
_inherit = "stock.move"
barcode_backorder_action = fields.Selection(
[
("pending", "Pending"),
("create_backorder", "Create Backorder"),
("skip_backorder", "No Backorder"),
],
string="Backorder action",
default="pending",
)
def _action_done(self, cancel_backorder=False):
moves_cancel_backorder = self.browse()
if not cancel_backorder:
moves_cancel_backorder = self.filtered(
lambda sm: sm.barcode_backorder_action == "skip_backorder"
)
super(StockMove, moves_cancel_backorder)._action_done(cancel_backorder=True)
moves_backorder = self - moves_cancel_backorder
moves_backorder.barcode_backorder_action = "pending"
return super(StockMove, moves_backorder)._action_done(
cancel_backorder=cancel_backorder
)
def copy_data(self, default=None):
vals_list = super().copy_data(default=default)
for vals in vals_list:
vals.pop("barcode_backorder_action", None)
return vals_list

View file

@ -0,0 +1,40 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
barcode_scan_state = fields.Selection(
[("pending", "Pending"), ("done", "Done"), ("done_forced", "Done forced")],
string="Scan State",
default="pending",
compute="_compute_barcode_scan_state",
readonly=False,
store=True,
)
@api.depends("qty_done", "reserved_uom_qty")
def _compute_barcode_scan_state(self):
for line in self:
if line.qty_done >= line.reserved_uom_qty:
line.barcode_scan_state = "done"
else:
line.barcode_scan_state = "pending"
def _barcodes_process_line_to_unlink(self):
self.qty_done = 0.0
def action_barcode_detailed_operation_unlink(self):
for sml in self:
stock_move = sml.move_id
stock_move.barcode_backorder_action = "pending"
sml.unlink()
# HACK: To force refresh wizard values
wiz_barcode = self.env["wiz.stock.barcodes.read.picking"].browse(
self.env.context.get("wiz_barcode_id", False)
)
stock_move._action_assign()
wiz_barcode.fill_todo_records()
wiz_barcode.determine_todo_action()

View file

@ -0,0 +1,73 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
class StockPicking(models.Model):
_inherit = "stock.picking"
def _prepare_barcode_wiz_vals(self, option_group):
vals = {
"picking_id": self.id,
"res_model_id": self.env.ref("stock.model_stock_picking").id,
"res_id": self.id,
"picking_type_code": self.picking_type_code,
"option_group_id": option_group.id,
"manual_entry": option_group.manual_entry,
"picking_mode": "picking",
}
if self.picking_type_id.code == "outgoing":
vals["location_dest_id"] = self.location_dest_id.id
elif self.picking_type_id.code == "incoming":
vals["location_id"] = self.location_id.id
if option_group.get_option_value("location_id", "filled_default"):
vals["location_id"] = self.location_id.id
if option_group.get_option_value("location_dest_id", "filled_default"):
vals["location_dest_id"] = self.location_dest_id.id
return vals
def action_barcode_scan(self, option_group=False):
option_group = (
option_group
or self.picking_type_id.barcode_option_group_id
or self.env.ref("stock_barcodes.stock_barcodes_option_group_operation")
)
wiz = self.env["wiz.stock.barcodes.read.picking"].create(
self._prepare_barcode_wiz_vals(option_group)
)
wiz.fill_pending_moves()
wiz.determine_todo_action()
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
action["res_id"] = wiz.id
return action
def button_validate(self):
put_in_pack_picks = self.filtered(
lambda p: p.picking_type_id.barcode_option_group_id.auto_put_in_pack
and not p.move_line_ids.result_package_id
)
if put_in_pack_picks:
put_in_pack_picks.action_put_in_pack()
if self.env.context.get("stock_barcodes_validate_picking", False):
res = super(
StockPicking, self.with_context(skip_backorder=True)
).button_validate()
else:
pickings_to_backorder = self._check_backorder()
if pickings_to_backorder:
return pickings_to_backorder._action_generate_backorder_wizard(
show_transfers=self._should_show_transfers()
)
res = super().button_validate()
if res is True and self.env.context.get("show_picking_type_action_tree", False):
res = self[:1].picking_type_id.get_action_picking_tree_ready()
if self.state == "done":
self.env["bus.bus"]._sendone(
"stock_barcodes_scan", "actions_barcode", {"valid_picking": True}
)
return res

View file

@ -0,0 +1,104 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from ast import literal_eval
from odoo import fields, models
class StockPickingType(models.Model):
_inherit = "stock.picking.type"
barcode_option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group"
)
new_picking_barcode_option_group_id = fields.Many2one(
comodel_name="stock.barcodes.option.group",
help="This Barcode Option Group will be selected when clicking the 'New' button"
" in an operation type. It will be used to create a non planned picking.",
)
def action_barcode_scan(self):
vals = {
"res_model_id": self.env.ref("stock.model_stock_picking_type").id,
"res_id": self.id,
"picking_type_code": self.code,
"option_group_id": self.barcode_option_group_id.id,
"manual_entry": self.barcode_option_group_id.manual_entry,
"picking_mode": "picking",
}
if self.code == "outgoing":
vals["location_dest_id"] = (
self.default_location_dest_id.id
or self.env.ref("stock.stock_location_customers").id
)
elif self.code == "incoming":
vals["location_id"] = (
self.default_location_src_id.id
or self.env.ref("stock.stock_location_suppliers").id
)
if self.barcode_option_group_id.get_option_value(
"location_id", "filled_default"
):
vals["location_id"] = self.default_location_src_id.id
if self.barcode_option_group_id.get_option_value(
"location_dest_id", "filled_default"
):
vals["location_dest_id"] = self.default_location_dest_id.id
wiz = self.env["wiz.stock.barcodes.read.picking"].create(vals)
wiz.fill_pending_moves()
wiz.determine_todo_action()
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
action["res_id"] = wiz.id
return action
def action_barcode_new_picking(self):
self.ensure_one()
picking = (
self.env["stock.picking"]
.with_context(default_immediate_transfer=True)
.create(
{
"picking_type_id": self.id,
"location_id": self.default_location_src_id.id,
"location_dest_id": self.default_location_dest_id.id,
}
)
)
option_group = self.new_picking_barcode_option_group_id
return picking.action_barcode_scan(option_group=option_group)
def get_action_picking_tree_ready(self):
context = dict(self.env.context)
if context.get("operations_mode", False):
return self._get_action(
"stock_barcodes.stock_barcodes_action_picking_tree_ready"
)
return super().get_action_picking_tree_ready()
def _get_action(self, action_xmlid):
action = self.env["ir.actions.actions"]._for_xml_id(action_xmlid)
if self:
action["display_name"] = self.display_name
default_immediate_tranfer = True
if (
self.env["ir.config_parameter"]
.sudo()
.get_param("stock.no_default_immediate_tranfer")
):
default_immediate_tranfer = False
context = {
"search_default_picking_type_id": [self.id],
"default_picking_type_id": self.id,
"default_immediate_transfer": default_immediate_tranfer,
"default_company_id": self.company_id.id,
}
action_context = literal_eval(action["context"].strip())
context = {**action_context, **context}
action["context"] = context
return action

View file

@ -0,0 +1,80 @@
# Copyright 2023 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import models
MODEL_UPDATE_INVENTORY = ["wiz.stock.barcodes.read.inventory"]
class StockQuant(models.Model):
_name = "stock.quant"
_inherit = ["stock.quant", "barcodes.barcode_events_mixin"]
def action_barcode_inventory_quant_unlink(self):
self.with_context(inventory_mode=True).action_set_inventory_quantity_to_zero()
context = dict(self.env.context)
params = context.get("params", {})
res_model = params.get("model", False)
res_id = params.get("id", False)
if res_id and res_model in MODEL_UPDATE_INVENTORY:
wiz_id = self.env[params["model"]].browse(params["id"])
wiz_id._compute_count_inventory_quants()
wiz_id.send_bus_done(
"stock_barcodes_form_update",
"count_apply_inventory",
{"count": wiz_id.count_inventory_quants},
)
def _get_fields_to_edit(self):
return [
"location_id",
"product_id",
"product_uom_id",
"lot_id",
"package_id",
]
def action_barcode_inventory_quant_edit(self):
wiz_barcode_id = self.env.context.get("wiz_barcode_id", False)
wiz_barcode = self.env["wiz.stock.barcodes.read.inventory"].browse(
wiz_barcode_id
)
for quant in self:
# Try to assign fields with the same name between quant and the scan wizard
for fname in self._get_fields_to_edit():
wiz_barcode[fname] = quant[fname]
wiz_barcode.product_qty = quant.inventory_quantity
wiz_barcode.manual_entry = True
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": True,
},
)
def enable_current_operations(self):
self.send_bus_done(
"stock_barcodes_kanban_update",
"enable_operations",
{
"id": self.id,
},
)
def operation_quantities_rest(self):
self.write({"inventory_quantity": self.inventory_quantity - 1})
self.enable_current_operations()
def operation_quantities(self):
self.write({"inventory_quantity": self.inventory_quantity + 1})
self.enable_current_operations()
def action_apply_inventory(self):
res = super().action_apply_inventory()
self.send_bus_done(
"stock_barcodes_scan",
"actions_barcode",
{"apply_inventory": True},
)
return res

View file

@ -0,0 +1,25 @@
* `Tecnativa <https://www.tecnativa.com>`_:
* Sergio Teruel
* Carlos Dauden
* Pedro M. Baeza
* Alexandre D. Díaz
* `Onestein <https://www.onestein.eu>`_:
* Andrea Stirpe
* `InitOS <https://www.initos.com>`_:
* Foram Shah
* `ForgeFlow <https://www.forgeflow.com>`_:
* Lois Rilo
* Enric Tobella
* `Binhex Cloud <https://www.binhex.cloud/>`_:
* Edilio Escalona Almira

View file

@ -0,0 +1,11 @@
This module provides a barcode reader interface for stock module.
This module contains a base wizard read barcode that can be extended by
other modules.
This module also makes use of this wizard for providing barcode support for
doing inventories and picking operations.
This module provides configuring barcodes for barcode actions.

View file

@ -0,0 +1,29 @@
11.0.1.1.0 (2019-09-24)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
User can uses barcode interface in picking operations.
13.0.1.1.1 (2021-02-06)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
Add option to get lots automatically based on removal strategy in inventory.
14.0.1.0.0 (2021-04-05)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] New feature.
Add security for users.
16.0.1.0.0 (2025-01-23)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP]
Improved views to optimize navigation and functionality.
Intuitive and mobile-friendly views.
Visual improvement of the main view accessed from the Barcodes menu.
* [ADD] New feature.
Barcode reading to barcode actions.
Generate PDF document for the barcodes of the selected barcode actions.

View file

@ -0,0 +1,4 @@
* Excute action_done() method outside onchange environment.
* Allow create product when a barcode has not been found.
* Allow to select picking reading its barcode.
* Allow to select multiple pickings to process scanned products.

View file

@ -0,0 +1,260 @@
Barcode interface for inventory operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Option 1: To use the barcode interface on inventory
#. Go to *Inventory > operations > Inventory Adjustments*.
#. Create new inventory with "Select products manually" option.
#. Start inventory.
#. Click to "Scan barcodes" smart button.
#. Start reading barcodes.
Option 2: Use the barcode interface inventory directly from the Barcodes application
#. Go to *Barcodes*.
#. Select the *Inventory* option.
.. image:: /stock_barcodes/static/src/img/inventory_barcode_action.png
:height: 100
:width: 200
:alt: Inventory barcode action
#. Start scanning barcodes.
Actions
# Press the *+ Product* button to display the form for the new item.
.. image:: /stock_barcodes/static/src/img/add_product.png
:height: 100
:width: 200
:alt: Add product
# When you select a product, a numeric field is displayed to add the quantity.
.. image:: /stock_barcodes/static/src/img/form_add_product_quantity.png
:height: 100
:width: 200
:alt: Add quantity product
# When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.
.. image:: /stock_barcodes/static/src/img/form_add_product_reset.png
:height: 100
:width: 200
:alt: Reset data form
# When you press the *Clean values* button, all fields are reset and the form is closed.
# When you press the *Confirm* 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.
.. image:: /stock_barcodes/static/src/img/list_items.png
:height: 100
:width: 200
:alt: Reset data form
# 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.
.. image:: /stock_barcodes/static/src/img/list_action_items.png
:height: 100
:width: 200
:alt: Reset data form
# The *Apply* 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.
.. image:: /stock_barcodes/static/src/img/apply_inventory.png
:height: 100
:width: 200
:alt: Apply inventory
.. image:: /stock_barcodes/static/src/img/apply_inventory_reason.png
:height: 100
:width: 200
:alt: Apply inventory reason
Barcode interface for picking operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
To use the barcode interface on picking operations:
#. Go to *Inventory*.
#. Click on scanner button on any operation type.
#. Start reading barcodes.
Option 1: To use the barcode interface on a picking:
#. Go to *Inventory > Transfers*.
#. Click to "Scan barcodes" smart button.
#. Start reading barcodes.
Option 2: Use the barcode interface picking directly from the Barcodes application
#. Go to *Barcodes*.
#. Select the option *OPERATIONS*.
.. image:: /stock_barcodes/static/src/img/inventory_barcode_action.png
:height: 100
:width: 200
:alt: Operation barcode action
# Select the type of picking.
# The pickings in ready status are displayed, select the one you want to start scanning.
.. image:: /stock_barcodes/static/src/img/list_picking.png
:height: 100
:width: 200
:alt: List picking
#. Start scanning barcodes.
.. image:: /stock_barcodes/static/src/img/barcode_interface_picking.png
:height: 100
:width: 200
:alt: List picking
Actions
# All the items that have been configured for the selected picking are listed.
.. image:: /stock_barcodes/static/src/img/list_items_picking.png
:height: 100
:width: 200
:alt: List picking
# The edit icon in the list allows you to modify the data.
.. image:: /stock_barcodes/static/src/img/list_items_picking_edit.png
:height: 100
:width: 200
:alt: Edit picking
# The button that contains a *+120* (in this case), allows you to define all the
remaining quantities. Once defined, this button disappears and if you want to change the
quantities, press the edit button.
.. image:: /stock_barcodes/static/src/img/list_items_picking_quantity.png
:height: 100
:width: 200
:alt: Quantity picking
# If there is at least one item with a quantity already defined, an eye icon is displayed,
which if closed shows the items and their quantities already scanned.
.. image:: /stock_barcodes/static/src/img/list_items_picking_scanned.png
:height: 100
:width: 200
:alt: Picking scanned
# When you press the *Validate* button, a wizard will be displayed to confirm the action.
If everything is correct, it is validated and you return to the picking list mentioned above.
.. image:: /stock_barcodes/static/src/img/confirm_items_picking.png
:height: 100
:width: 200
:alt: Picking scanned
# If there is an item whose quantity is zero, a wizard will be displayed after the one mentioned
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.
.. image:: /stock_barcodes/static/src/img/confirm_all_quantity_items_picking.png
:height: 100
:width: 200
:alt: Picking scanned
# Press the *+ Product* button to display the form for the new item.
.. image:: /stock_barcodes/static/src/img/add_product.png
:height: 100
:width: 200
:alt: Add product
# When you select a product, a numeric field is displayed to add the quantity.
.. image:: /stock_barcodes/static/src/img/form_add_product_quantity.png
:height: 100
:width: 200
:alt: Add quantity product
# When you press the button with the trash can icon, the values of the form are reset (except for the location) without closing it.
.. image:: /stock_barcodes/static/src/img/form_add_product_reset.png
:height: 100
:width: 200
:alt: Reset data form
# When you press the *Clean values* button, all fields are reset and the form is closed.
# When you press the *Confirm* 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.
The barcode scanner interface has two operation modes. In both of them user
can scan:
#. Warehouse locations with barcode.
#. Product packaging with barcode.
#. Product with barcode.
#. Product Lots (The barcode is name field in this case).
Automatic operation mode
~~~~~~~~~~~~~~~~~~~~~~~~
This is the default mode, all screen controls are locked to avoid scan into
fields.
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:
#. 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).
#. 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.
#. 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.
#. Try to search a location, if found the field location is set and next scan
action will be done with this warehouse location.
If barcode has not found, when message is displayed you can create this lot
scanning the product.
Manual entry mode
~~~~~~~~~~~~~~~~~
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.
Scan logs
~~~~~~~~~
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.
Barcode interface for barcode actions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use the barcode interface for actions:
#. Go to *Inventory > Configuration > Barcode Actions*.
#. Create a new barcode action and configure the barcode.
.. image:: /stock_barcodes/static/src/img/create_barcode_action.png
:height: 100
:width: 200
:alt: Print barcodes
#. Select the barcode actions you want to use, a button (PRINT BARCODES) will appear that allows you to print the configured barcodes to PDF.
.. image:: /stock_barcodes/static/src/img/print_barcodes.png
:height: 100
:width: 200
:alt: Print barcodes
#. Go to *Barcodes*.
#. Start scanning barcodes from actions.

View file

@ -0,0 +1 @@
from . import barcode_actions_report

View file

@ -0,0 +1,16 @@
from odoo import api, models
class ReportStockBarcodesBarcodeActions(models.Model):
_name = "report.stock_barcodes.report_barcode_actions"
_description = "Print barcodes from barcode actions"
@api.model
def _get_report_values(self, docids, data=None):
datas = self.env["stock.barcodes.action"].search_read(
[("id", "in", docids), ("barcode", "!=", False)],
["name", "barcode", "barcode_image"],
)
return {
"barcodes": datas,
}

View file

@ -0,0 +1,25 @@
<odoo>
<template id="report_barcode_actions">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<div class="page">
<div class="row text-center mb-4">
<h1>Scan your actions and streamline your actions.</h1>
</div>
<div class="row">
<t t-foreach="barcodes" t-as="barcode">
<div class="col-4 text-center">
<h2 t-out="barcode['name']" />
<img
t-att-src="'data:image/png;base64,%s' % barcode['barcode_image'].decode('utf-8')"
width="100%"
height="160px"
/>
</div>
</t>
</div>
</div>
</t>
</t>
</template>
</odoo>

View file

@ -0,0 +1,28 @@
<odoo>
<record id="paperformat_barcode_actions" model="report.paperformat">
<field name="name">A4</field>
<field name="default" eval="True" />
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">30</field>
<field name="margin_bottom">5</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False" />
<field name="header_spacing">5</field>
<field name="dpi">90</field>
</record>
<record id="action_report_barcode_actions" model="ir.actions.report">
<field name="name">Barcodes (PDF)</field>
<field name="model">stock.barcodes.action</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">stock_barcodes.report_barcode_actions</field>
<field name="report_file">stock_barcodes.report_barcode_actions</field>
<field name="print_report_name">'Barcodes - %s' % (object.name)</field>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_barcode_actions" />
</record>
</odoo>

View file

@ -0,0 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_wiz_stock_barcodes_read_picking,access_wiz_stock_barcodes_read_picking,model_wiz_stock_barcodes_read_picking,base.group_user,1,1,1,1
access_wiz_candidate_picking,access_wiz_candidate_picking,model_wiz_candidate_picking,base.group_user,1,1,1,1
access_wiz_stock_barcodes_new_lot,access_wiz_stock_barcodes_new_lot,model_wiz_stock_barcodes_new_lot,base.group_user,1,1,1,1
access_stock_barcodes_action_manager,access_stock_barcodes_action_manager,model_stock_barcodes_action,base.group_system,1,1,1,1
access_stock_barcodes_action,access_stock_barcodes_action,model_stock_barcodes_action,base.group_user,1,0,0,0
access_stock_barcodes_option_group,access_stock_barcodes_option_group,model_stock_barcodes_option_group,base.group_user,1,1,1,1
access_stock_barcodes_option,access_stock_barcodes_option,model_stock_barcodes_option,base.group_user,1,1,1,1
access_stock_barcodes_read_picking,access_stock_barcodes_read_picking,model_wiz_stock_barcodes_read_picking,base.group_user,1,1,1,1
access_wiz_stock_barcodes_read_todo,access_wiz_stock_barcodes_read_todo,model_wiz_stock_barcodes_read_todo,base.group_user,1,1,1,1
access_wiz_stock_barcodes_read_inventory,access_wiz_stock_barcodes_read_inventory,model_wiz_stock_barcodes_read_inventory,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_wiz_stock_barcodes_read_picking access_wiz_stock_barcodes_read_picking model_wiz_stock_barcodes_read_picking base.group_user 1 1 1 1
3 access_wiz_candidate_picking access_wiz_candidate_picking model_wiz_candidate_picking base.group_user 1 1 1 1
4 access_wiz_stock_barcodes_new_lot access_wiz_stock_barcodes_new_lot model_wiz_stock_barcodes_new_lot base.group_user 1 1 1 1
5 access_stock_barcodes_action_manager access_stock_barcodes_action_manager model_stock_barcodes_action base.group_system 1 1 1 1
6 access_stock_barcodes_action access_stock_barcodes_action model_stock_barcodes_action base.group_user 1 0 0 0
7 access_stock_barcodes_option_group access_stock_barcodes_option_group model_stock_barcodes_option_group base.group_user 1 1 1 1
8 access_stock_barcodes_option access_stock_barcodes_option model_stock_barcodes_option base.group_user 1 1 1 1
9 access_stock_barcodes_read_picking access_stock_barcodes_read_picking model_wiz_stock_barcodes_read_picking base.group_user 1 1 1 1
10 access_wiz_stock_barcodes_read_todo access_wiz_stock_barcodes_read_todo model_wiz_stock_barcodes_read_todo base.group_user 1 1 1 1
11 access_wiz_stock_barcodes_read_inventory access_wiz_stock_barcodes_read_inventory model_wiz_stock_barcodes_read_inventory base.group_user 1 1 1 1

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>

View file

@ -0,0 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_stock_barcodes
from . import test_stock_barcodes_new_lot
from . import test_stock_barcodes_picking

View file

@ -0,0 +1,192 @@
# Copyright 2108-2019 Francois Poizat <francois.poizat@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import TransactionCase
class TestCommonStockBarcodes(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Active group_stock_packaging and group_production_lot for user
group_stock_packaging = cls.env.ref("product.group_stock_packaging")
group_production_lot = cls.env.ref("stock.group_production_lot")
cls.env.user.groups_id = [
(4, group_stock_packaging.id),
(4, group_production_lot.id),
]
# models
cls.StockLocation = cls.env["stock.location"]
cls.Product = cls.env["product.product"]
cls.ProductPackaging = cls.env["product.packaging"]
cls.WizScanReadPicking = cls.env["wiz.stock.barcodes.read.picking"]
cls.WizScanReadInventory = cls.env["wiz.stock.barcodes.read.inventory"]
cls.WizCandidatePicking = cls.env["wiz.candidate.picking"]
cls.StockProductionLot = cls.env["stock.lot"]
cls.StockPicking = cls.env["stock.picking"]
cls.StockQuant = cls.env["stock.quant"]
cls.StockBarcodeAction = cls.env["stock.barcodes.action"]
cls.company = cls.env.company
# Option groups for test
cls.option_group = cls._create_barcode_option_group()
# warehouse and locations
cls.warehouse = cls.env.ref("stock.warehouse0")
cls.stock_location = cls.env.ref("stock.stock_location_stock")
cls.location_1 = cls.StockLocation.create(
{
"name": "Test location 1",
"usage": "internal",
"location_id": cls.stock_location.id,
"barcode": "8411322222568",
}
)
cls.location_2 = cls.StockLocation.create(
{
"name": "Test location 2",
"usage": "internal",
"location_id": cls.stock_location.id,
"barcode": "8470001809032",
}
)
# products
cls.product_wo_tracking = cls.Product.create(
{
"name": "Product test wo lot tracking",
"type": "product",
"tracking": "none",
"barcode": "8480000723208",
"packaging_ids": [
(
0,
0,
{
"name": "Box 10 Units",
"qty": 10.0,
"barcode": "5099206074439",
},
)
],
}
)
cls.product_tracking = cls.Product.create(
{
"name": "Product test with lot tracking",
"type": "product",
"tracking": "lot",
"barcode": "8433281006850",
"packaging_ids": [
(
0,
0,
{"name": "Box 5 Units", "qty": 5.0, "barcode": "5420008510489"},
)
],
}
)
cls.lot_1 = cls.StockProductionLot.create(
{
"name": "8411822222568",
"product_id": cls.product_tracking.id,
"company_id": cls.company.id,
}
)
cls.quant_lot_1 = cls.StockQuant.create(
{
"product_id": cls.product_tracking.id,
"lot_id": cls.lot_1.id,
"location_id": cls.stock_location.id,
"quantity": 100.0,
}
)
cls.wiz_scan = cls.WizScanReadPicking.create(
{"option_group_id": cls.option_group.id, "step": 1}
)
cls.wiz_scan_read_inventory = cls.WizScanReadInventory.create(
{"option_group_id": cls.option_group.id, "step": 1}
)
cls.wiz_scan_candidate_picking = cls.WizCandidatePicking.create(
{"wiz_barcode_id": cls.wiz_scan.id}
)
# Barcode actions
cls.barcode_action_valid = cls.StockBarcodeAction.create(
{
"name": "Barcode action valid",
"action_window_id": cls.env.ref("stock.stock_picking_type_action").id,
"context": "{'search_default_barcode_options': 1}",
}
)
cls.barcode_action_invalid = cls.StockBarcodeAction.create(
{
"name": "Barcode action valid",
}
)
@classmethod
def _create_barcode_option_group(cls):
return cls.env["stock.barcodes.option.group"].create(
{
"name": "option group for tests",
"create_lot": True,
"option_ids": [
(
0,
0,
{
"step": 1,
"name": "Location",
"field_name": "location_id",
"to_scan": True,
"required": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Product",
"field_name": "product_id",
"to_scan": True,
"required": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Packaging",
"field_name": "packaging_id",
"to_scan": True,
"required": False,
},
),
(
0,
0,
{
"step": 2,
"name": "Lot / Serial",
"field_name": "lot_id",
"to_scan": True,
"required": True,
},
),
],
}
)
def action_barcode_scanned(self, wizard, barcode):
wizard._barcode_scanned = barcode
wizard._on_barcode_scanned()
# Method to call all methods outside of onchange environment for pickings read
if wizard._name != "wiz.stock.barcodes.new.lot":
wizard.dummy_on_barcode_scanned()

View file

@ -0,0 +1,141 @@
# Copyright 2108-2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import re
from odoo.exceptions import ValidationError
from odoo.tests.common import tagged
from odoo.addons.stock_barcodes.models.stock_barcodes_action import FIELDS_NAME, REGEX
from .common import TestCommonStockBarcodes
@tagged("post_install", "-at_install")
class TestStockBarcodes(TestCommonStockBarcodes):
def test_wizard_scan_location(self):
self.action_barcode_scanned(self.wiz_scan, "8411322222568")
self.assertEqual(self.wiz_scan.location_id, self.location_1)
def test_wizard_scan_product(self):
self.wiz_scan.location_id = self.location_1
self.wiz_scan.action_show_step()
self.action_barcode_scanned(self.wiz_scan, "8480000723208")
self.assertEqual(self.wiz_scan.product_id, self.product_wo_tracking)
self.assertEqual(self.wiz_scan.product_qty, 1.0)
def test_wizard_scan_product_manual_entry(self):
# Test manual entry
self.wiz_scan.manual_entry = True
self.wiz_scan.location_id = self.location_1
self.wiz_scan.action_show_step()
self.action_barcode_scanned(self.wiz_scan, "8480000723208")
self.assertEqual(self.wiz_scan.product_qty, 0.0)
self.wiz_scan.product_qty = 50.0
def test_wizard_scan_package(self):
self.wiz_scan.location_id = self.location_1
self.wiz_scan.action_show_step()
self.action_barcode_scanned(self.wiz_scan, "5420008510489")
self.assertEqual(self.wiz_scan.product_id, self.product_tracking)
self.assertEqual(self.wiz_scan.product_qty, 5.0)
self.assertEqual(
self.wiz_scan.packaging_id, self.product_tracking.packaging_ids
)
# Manual entry
self.wiz_scan.manual_entry = True
self.wiz_scan.action_clean_values()
self.action_barcode_scanned(self.wiz_scan, "5420008510489")
self.assertEqual(self.wiz_scan.packaging_qty, 1.0)
self.wiz_scan.packaging_qty = 3.0
self.wiz_scan.onchange_packaging_qty()
self.assertEqual(self.wiz_scan.product_qty, 15.0)
self.wiz_scan.manual_entry = False
def test_wizard_scan_lot(self):
self.wiz_scan.location_id = self.location_1.id
self.wiz_scan.action_show_step()
self.action_barcode_scanned(self.wiz_scan, "8411822222568")
# Lot found for one product, so product_id is filled
self.assertTrue(self.wiz_scan.product_id)
self.action_barcode_scanned(self.wiz_scan, "8433281006850")
self.action_barcode_scanned(self.wiz_scan, "8411822222568")
self.assertEqual(self.wiz_scan.lot_id, self.lot_1)
# After scan other product, set wizard lot to False
self.action_barcode_scanned(self.wiz_scan, "8480000723208")
self.assertFalse(self.wiz_scan.lot_id)
def test_wizard_scan_not_found(self):
self.action_barcode_scanned(self.wiz_scan, "84118xxx22568")
self.assertEqual(
self.wiz_scan.message,
"84118xxx22568 (Barcode not found with this screen values)",
)
def test_wiz_clean_lot(self):
self.wiz_scan.location_id = self.location_1.id
self.wiz_scan.action_show_step()
self.action_barcode_scanned(self.wiz_scan, "8433281006850")
self.action_barcode_scanned(self.wiz_scan, "8411822222568")
self.wiz_scan.action_clean_lot()
self.assertFalse(self.wiz_scan.lot_id)
def test_barcode_action(self):
self.assertTrue(self.barcode_action_valid.action_window_id)
self.assertEqual(bool(self.barcode_action_invalid.action_window_id), False)
def test_action_back(self):
result = self.wiz_scan.action_back()
self.assertIn("name", result)
self.assertIn("type", result)
self.assertIn("res_model", result)
self.assertEqual(result["type"], "ir.actions.act_window")
def test_barcode_context_action(self):
context = self.barcode_action_valid.context
self.assertTrue(bool(re.match(REGEX.get("context", ""), context)))
self.assertGreater(len(context), 0)
context = context.strip("{}").split(",")
field_values = context[0].split(":")
self.assertGreater(len(field_values), 1)
field_name = field_values[0].split("search_default_")
self.assertGreater(len(field_name), 1)
field_value_format = field_values[1].replace("'", "").strip()
self.assertTrue(field_value_format.isdigit())
self.assertEqual(field_values[0].strip("'"), "search_default_barcode_options")
self.assertTrue(len(field_values[0].split("search_default_")), 2)
self.assertEqual(self.barcode_action_invalid._count_elements(), 0)
self.barcode_action_invalid.context = False
with self.assertRaises(TypeError):
self.barcode_action_invalid._compute_count_elements()
self.barcode_action_invalid.context = "{}"
self.assertFalse("search_default_" in self.barcode_action_invalid.context)
self.assertEqual(self.barcode_action_invalid._count_elements(), 0)
self.barcode_action_valid.context = "{'search_default_code': 1}"
self.assertEqual(self.barcode_action_valid._count_elements(), 6)
field_value_name = (
self.barcode_action_valid.context.strip("{}").split(",")[0].split(":")
)
field_name = field_value_name[0].split("search_default_")[1].strip("'")
self.assertTrue("search_default_" in self.barcode_action_valid.context)
self.assertFalse(
hasattr(
self.barcode_action_valid.action_window_id.res_model,
FIELDS_NAME.get(field_name, field_name),
)
)
field_values = field_value_name[1].strip()
self.assertTrue(field_values.isdigit())
with self.assertRaises(IndexError):
self.barcode_action_invalid.context = "{'search_default_'}"
self.assertEqual(self.barcode_action_invalid._count_elements(), 0)
with self.assertRaises(ValidationError):
self.StockBarcodeAction.create(
{
"name": "Barcode action invalid with space",
"context": "{'search_default_code': 'incoming'} ",
}
)

View file

@ -0,0 +1,25 @@
# Copyright 2108-2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import tagged
from .common import TestCommonStockBarcodes
@tagged("post_install", "-at_install")
class TestStockBarcodesNewLot(TestCommonStockBarcodes):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ScanReadLot = cls.env["wiz.stock.barcodes.new.lot"]
cls.wiz_scan_lot = cls.ScanReadLot.new()
def test_new_lot(self):
self.action_barcode_scanned(self.wiz_scan_lot, "8433281006850")
self.assertEqual(self.wiz_scan_lot.product_id, self.product_tracking)
self.action_barcode_scanned(self.wiz_scan_lot, "8433281xy6850")
self.assertEqual(self.wiz_scan_lot.lot_name, "8433281xy6850")
self.wiz_scan_lot.with_context(
active_model=self.wiz_scan._name,
active_id=self.wiz_scan.id,
).confirm()
self.assertEqual(self.wiz_scan_lot.lot_name, self.wiz_scan.lot_id.name)

View file

@ -0,0 +1,539 @@
# Copyright 2108-2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import MissingError, UserError
from odoo.tests.common import tagged
from .common import TestCommonStockBarcodes
@tagged("post_install", "-at_install")
class TestStockBarcodesPicking(TestCommonStockBarcodes):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ScanReadPicking = cls.env["wiz.stock.barcodes.read.picking"]
cls.stock_picking_model = cls.env.ref("stock.model_stock_picking")
# Model Data
cls.barcode_option_group_out = cls._create_barcode_option_group_outgoing()
cls.barcode_option_group_in = cls._create_barcode_option_group_incoming()
cls.barcode_option_group_out.barcode_guided_mode = False
cls.barcode_option_group_in.barcode_guided_mode = False
cls.partner_agrolite = cls.env.ref("base.res_partner_2")
cls.picking_type_in = cls.env.ref("stock.picking_type_in")
cls.picking_type_in.barcode_option_group_id = cls.barcode_option_group_in
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
cls.picking_type_out.reservation_method = "manual"
cls.picking_type_out.barcode_option_group_id = cls.barcode_option_group_out
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
cls.customer_location = cls.env.ref("stock.stock_location_customers")
cls.stock_location = cls.env.ref("stock.stock_location_stock")
cls.categ_unit = cls.env.ref("uom.product_uom_categ_unit")
cls.categ_kgm = cls.env.ref("uom.product_uom_categ_kgm")
cls.picking_out_01 = (
cls.env["stock.picking"]
.with_context(planned_picking=True)
.create(
{
"location_id": cls.stock_location.id,
"location_dest_id": cls.customer_location.id,
"partner_id": cls.partner_agrolite.id,
"picking_type_id": cls.picking_type_out.id,
"move_ids": [
(
0,
0,
{
"name": cls.product_tracking.name,
"product_id": cls.product_tracking.id,
"product_uom_qty": 3,
"product_uom": cls.product_tracking.uom_id.id,
"location_id": cls.stock_location.id,
"location_dest_id": cls.customer_location.id,
},
)
],
}
)
)
cls.picking_out_02 = cls.picking_out_01.copy()
cls.picking_in_01 = (
cls.env["stock.picking"]
.with_context(planned_picking=True)
.create(
{
"location_id": cls.supplier_location.id,
"location_dest_id": cls.stock_location.id,
"partner_id": cls.partner_agrolite.id,
"picking_type_id": cls.picking_type_in.id,
"move_ids": [
(
0,
0,
{
"name": cls.product_wo_tracking.name,
"product_id": cls.product_wo_tracking.id,
"product_uom_qty": 3,
"product_uom": cls.product_wo_tracking.uom_id.id,
"location_id": cls.supplier_location.id,
"location_dest_id": cls.stock_location.id,
},
),
(
0,
0,
{
"name": cls.product_wo_tracking.name,
"product_id": cls.product_wo_tracking.id,
"product_uom_qty": 5,
"product_uom": cls.product_wo_tracking.uom_id.id,
"location_id": cls.supplier_location.id,
"location_dest_id": cls.stock_location.id,
},
),
(
0,
0,
{
"name": cls.product_tracking.name,
"product_id": cls.product_tracking.id,
"product_uom_qty": 3,
"product_uom": cls.product_tracking.uom_id.id,
"location_id": cls.supplier_location.id,
"location_dest_id": cls.stock_location.id,
},
),
(
0,
0,
{
"name": cls.product_tracking.name,
"product_id": cls.product_tracking.id,
"product_uom_qty": 5,
"product_uom": cls.product_tracking.uom_id.id,
"location_id": cls.supplier_location.id,
"location_dest_id": cls.stock_location.id,
},
),
],
}
)
)
cls.picking_in_01.action_confirm()
action = cls.picking_in_01.action_barcode_scan()
cls.wiz_scan_picking = cls.ScanReadPicking.browse(action["res_id"])
# Create a wizard for outgoing picking
cls.picking_out_01.action_confirm()
action = cls.picking_out_01.action_barcode_scan()
cls.wiz_scan_picking_out = cls.ScanReadPicking.browse(action["res_id"])
def test_wiz_picking_values(self):
self.assertEqual(
self.wiz_scan_picking.location_id, self.picking_in_01.location_id
)
self.assertEqual(self.wiz_scan_picking.res_model_id, self.stock_picking_model)
self.assertEqual(self.wiz_scan_picking.res_id, self.picking_in_01.id)
self.assertIn(
"Barcode reader - %s - " % (self.picking_in_01.name),
self.wiz_scan_picking.display_name,
)
def test_picking_wizard_scan_product(self):
# self.wiz_scan_picking.manual_entry = True
wiz_scan_picking = self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
)
self.action_barcode_scanned(wiz_scan_picking, "8480000723208")
sml = self.picking_in_01.move_line_ids.filtered(
lambda x: x.product_id == self.product_wo_tracking
)
self.assertEqual(sml.qty_done, 1.0)
# Scan product with tracking lot enable
self.action_barcode_scanned(wiz_scan_picking, "8433281006850")
sml = self.picking_in_01.move_line_ids.filtered(
lambda x: x.product_id == self.product_tracking
)
self.assertEqual(sml.qty_done, 0.0)
self.assertEqual(
self.wiz_scan_picking.message,
"8433281006850 (Scan Product, Packaging, Lot / Serial)",
)
# Scan a lot. Increment quantities if scan product or other lot from
# this product
self.action_barcode_scanned(wiz_scan_picking, "8411822222568")
sml = self.picking_in_01.move_line_ids.filtered(
lambda x: x.product_id == self.product_tracking and x.lot_id
)
self.assertEqual(sml.lot_id, self.lot_1)
self.assertEqual(sml.qty_done, 1.0)
self.action_barcode_scanned(wiz_scan_picking, "8433281006850")
stock_move = sml.move_id
self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 1.0)
self.action_barcode_scanned(wiz_scan_picking, "8411822222568")
self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 1.0)
self.assertEqual(
self.wiz_scan_picking.message,
"8411822222568 (Scan Product, Packaging, Lot / Serial)",
)
# Scan a package
self.action_barcode_scanned(wiz_scan_picking, "5420008510489")
# Package of 5 product units. Already three unit exists
self.assertEqual(sum(stock_move.move_line_ids.mapped("qty_done")), 5.0)
def test_picking_wizard_scan_product_manual_entry(self):
wiz_scan_picking = self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
)
wiz_scan_picking.manual_entry = True
self.action_barcode_scanned(wiz_scan_picking, "8480000723208")
sml = self.picking_in_01.move_line_ids.filtered(
lambda x: x.product_id == self.product_wo_tracking
)
self.assertEqual(wiz_scan_picking.product_qty, 0.0)
wiz_scan_picking.product_qty = 12.0
wiz_scan_picking.action_confirm()
self.assertEqual(sml.qty_done, 12.0)
def test_barcode_from_operation(self):
picking_out_3 = self.picking_out_01.copy()
self.picking_out_01.action_assign()
self.picking_out_02.action_assign()
self.picking_type_out.default_location_dest_id = self.customer_location
action = self.picking_type_out.action_barcode_scan()
self.wiz_scan_picking = self.ScanReadPicking.browse(action["res_id"])
self.wiz_scan_picking.manual_entry = True
self.wiz_scan_picking.product_id = self.product_tracking
self.wiz_scan_picking.lot_id = self.lot_1
self.wiz_scan_picking.product_qty = 2
self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
).action_confirm()
self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:2]), 2)
# Lock first picking
candidate = self.wiz_scan_picking.candidate_picking_ids.filtered(
lambda c: c.picking_id == self.picking_out_01
)
candidate_wiz = candidate.with_context(
wiz_barcode_id=self.wiz_scan_picking.id, picking_id=self.picking_out_01.id
)
candidate_wiz.with_context(force_create_move=True).action_lock_picking()
self.assertEqual(self.picking_out_01.move_ids.quantity_done, 2)
self.wiz_scan_picking.product_qty = 2
self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
).action_confirm()
self.assertEqual(self.picking_out_01.move_ids.quantity_done, 2)
# Picking out 3 is in confirmed state, so until confirmed moves has
# not been activated candidate pickings is 2
picking_out_3.action_confirm()
candidate_wiz.action_unlock_picking()
self.wiz_scan_picking.product_qty = 2
self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
).action_confirm()
self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:2]), 2)
candidate_wiz.action_unlock_picking()
self.wiz_scan_picking.product_qty = 2
self.wiz_scan_picking.option_group_id.confirmed_moves = True
self.wiz_scan_picking.with_context(
force_create_move=True, no_increase_qty_done=True
).action_confirm()
self.assertEqual(len(self.wiz_scan_picking.candidate_picking_ids[0:3]), 3)
def test_picking_wizard_scan_product_auto_lot(self):
# Prepare more data
lot_2 = self.StockProductionLot.create(
{
"name": "8411822222578",
"product_id": self.product_tracking.id,
"company_id": self.company.id,
}
)
lot_3 = self.StockProductionLot.create(
{
"name": "8411822222588",
"product_id": self.product_tracking.id,
"company_id": self.company.id,
}
)
quant_lot_2 = self.StockQuant.create(
{
"product_id": self.product_tracking.id,
"lot_id": lot_2.id,
"location_id": self.stock_location.id,
"quantity": 15.0,
}
)
quant_lot_3 = self.StockQuant.create(
{
"product_id": self.product_tracking.id,
"lot_id": lot_3.id,
"location_id": self.stock_location.id,
"quantity": 10.0,
}
)
self.quant_lot_1.in_date = "2021-01-01"
quant_lot_2.in_date = "2021-01-05"
quant_lot_3.in_date = "2021-01-06"
# Scan product with tracking lot enable
self.action_barcode_scanned(self.wiz_scan_picking, "8433281006850")
self.assertEqual(
self.wiz_scan_picking.message,
"8433281006850 (Scan Product, Packaging, Lot / Serial)",
)
self.wiz_scan_picking.auto_lot = True
# self.wiz_scan_picking.manual_entry = True
# Removal strategy FIFO
# No auto lot for incoming pickings
self.action_barcode_scanned(self.wiz_scan_picking, "8433281006850")
self.assertFalse(self.wiz_scan_picking.lot_id)
# Continue test with a outgoing wizard
self.wiz_scan_picking_out.option_group_id.auto_lot = True
self.wiz_scan_picking_out.auto_lot = True
self.action_barcode_scanned(self.wiz_scan_picking_out, "8433281006850")
self.assertEqual(self.wiz_scan_picking_out.lot_id, self.lot_1)
# Removal strategy LIFO
self.wiz_scan_picking_out.lot_id = False
self.product_tracking.categ_id.removal_strategy_id = self.env.ref(
"stock.removal_lifo"
)
self.wiz_scan_picking_out.action_clean_values()
self.action_barcode_scanned(self.wiz_scan_picking_out, "8433281006850")
self.assertEqual(self.wiz_scan_picking_out.lot_id, lot_3)
@classmethod
def _create_barcode_option_group_incoming(cls):
return cls.env["stock.barcodes.option.group"].create(
{
"name": "option group incoming for tests",
"option_ids": [
(
0,
0,
{
"step": 1,
"name": "Location",
"field_name": "location_id",
"filled_default": True,
"to_scan": False,
"required": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Product",
"field_name": "product_id",
"to_scan": True,
"required": True,
"clean_after_done": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Packaging",
"field_name": "packaging_id",
"to_scan": True,
"required": False,
},
),
(
0,
0,
{
"step": 2,
"name": "Lot / Serial",
"field_name": "lot_id",
"to_scan": True,
"required": True,
},
),
(
0,
0,
{
"step": 3,
"name": "Location Dest",
"field_name": "location_dest_id",
"filled_default": True,
"to_scan": False,
"required": True,
},
),
(
0,
0,
{
"step": 4,
"name": "Quantity",
"field_name": "product_qty",
"required": True,
"clean_after_done": True,
},
),
],
}
)
@classmethod
def _create_barcode_option_group_outgoing(cls):
return cls.env["stock.barcodes.option.group"].create(
{
"name": "option group outgoing for tests",
"option_ids": [
(
0,
0,
{
"step": 1,
"name": "Location",
"field_name": "location_id",
"to_scan": True,
"required": True,
"filled_default": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Product",
"field_name": "product_id",
"to_scan": True,
"required": True,
},
),
(
0,
0,
{
"step": 2,
"name": "Packaging",
"field_name": "packaging_id",
"to_scan": True,
"required": False,
},
),
(
0,
0,
{
"step": 2,
"name": "Lot / Serial",
"field_name": "lot_id",
"to_scan": True,
"required": True,
},
),
(
0,
0,
{
"step": 3,
"name": "Location Dest",
"field_name": "location_dest_id",
"filled_default": True,
"to_scan": False,
"required": True,
},
),
(
0,
0,
{
"step": 4,
"name": "Quantity",
"field_name": "product_qty",
"required": True,
"clean_after_done": True,
},
),
],
}
)
def test_stock_picking_validate(self):
self.picking_in_01.state = False
with self.assertRaises(UserError):
self.picking_in_01.with_context(
stock_barcodes_validate_picking=True
).button_validate()
def test_barcode_read_picking(self):
self.picking_in_01.state = "done"
self.wiz_scan_picking._compute_enable_add_product()
self.assertFalse(self.wiz_scan_picking.enable_add_product)
self.wiz_scan_picking.show_detailed_operations = False
self.wiz_scan_picking.action_show_detailed_operations()
self.assertTrue(self.wiz_scan_picking.action_show_detailed_operations)
self.wiz_scan_picking.action_show_detailed_operations()
self.assertFalse(self.wiz_scan_picking.show_detailed_operations)
def test_barcode_read_inventory(self):
context = {
"params": {
"model": "wiz.stock.barcodes.read.inventory",
"id": self.quant_lot_1.id,
}
}
with self.assertRaises(MissingError):
self.quant_lot_1.with_context(
**context
).action_barcode_inventory_quant_unlink()
context = {
"params": {
"model": self.wiz_scan_read_inventory._name,
"id": self.wiz_scan_read_inventory.id,
}
}
self.quant_lot_1.with_context(**context).action_barcode_inventory_quant_unlink()
self.assertIsNone(
self.quant_lot_1.with_context(
**context
).action_barcode_inventory_quant_unlink()
)
self.assertIsNone(self.quant_lot_1.enable_current_operations())
self.assertIsNone(self.quant_lot_1.action_barcode_inventory_quant_edit())
with self.assertRaises(ValueError):
self.quant_lot_1.write({"inventory_quantity": "test"})
self.quant_lot_1.operation_quantities_rest()
self.quant_lot_1.operation_quantities()
self.assertEqual(
type(self.picking_in_01.picking_type_id.get_action_picking_tree_ready()),
dict,
)
self.assertEqual(
type(
self.picking_in_01.picking_type_id.with_context(
**{"operations_mode": True}
).get_action_picking_tree_ready()
),
dict,
)
self.assertIsNone(self.wiz_scan_candidate_picking._compute_picking_quantity())
self.assertIsNone(self.wiz_scan_candidate_picking._compute_is_pending())
self.assertEqual(
self.wiz_scan_candidate_picking._get_picking_to_validate()._name,
self.picking_in_01._name,
)
self.assertEqual(
type(self.wiz_scan_candidate_picking.action_validate_picking()), tuple
)

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Actions inside the menu -->
<record id="view_stock_barcodes_action_tree" model="ir.ui.view">
<field name="name">stock.barcodes.action.tree</field>
<field name="model">stock.barcodes.action</field>
<field name="arch" type="xml">
<tree editable="top">
<header>
<button
string="Print barcodes"
type="object"
name="print_barcodes"
/>
</header>
<field name="sequence" widget="handle" />
<field name="name" />
<field name="action_window_id" />
<field name="context" />
<field name="key_shortcut" />
<field name="key_char_shortcut" />
<field name="active" widget="boolean_toggle" />
<field name="barcode" />
<field name="icon_class" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes">
<field name="res_model">stock.barcodes.action</field>
<field name="name">Barcodes actions</field>
<field name="view_mode">tree</field>
</record>
<record id="view_stock_barcodes_action_kanban" model="ir.ui.view">
<field name="name">stock.barcodes.action.kanban</field>
<field name="model">stock.barcodes.action</field>
<field name="arch" type="xml">
<kanban
class="o_kanban_mobile stock_barcodes_action_kanban"
create="0"
edit="0"
delete="0"
action="open_action"
type="object"
>
<field name="name" />
<field name="action_window_id" />
<field name="key_shortcut" />
<field name="key_char_shortcut" />
<field name="icon_class" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_content">
<t t-set="shortcut" t-value="record.key_char_shortcut" />
<t
t-set="hotkey"
t-value="context.context_display_menu &amp;&amp; record.key_char_shortcut.raw_value || ''"
/>
<div class="d-flex justify-content-between">
<div>
<i
t-if="record.icon_class.raw_value != false"
t-attf-class="mx-2 #{record.icon_class.raw_value}"
/>
<field name="name" />
</div>
<div class="rounded-circle count-elements">
<field name="count_elements" />
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Main menu from scan wizard -->
<record id="action_stock_barcodes_action_kanban" model="ir.actions.act_window">
<field name="res_model">stock.barcodes.action</field>
<field name="name">Barcodes</field>
<field name="view_mode">kanban</field>
<field name="view_id" ref="view_stock_barcodes_action_kanban" />
<field name="domain">[('action_window_id', '!=', False)]</field>
<field name="target">fullscreen</field>
</record>
<record id="action_stock_barcodes_action_client" model="ir.actions.client">
<field name="name">Barcodes</field>
<field name="tag">stock_barcodes_main_menu</field>
<field name="target">fullscreen</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes_action">
<field name="res_model">wiz.stock.barcodes.read.picking</field>
<field name="name">Barcodes actions</field>
<field name="view_mode">form</field>
<field
name="context"
>{'control_panel_hidden': True, 'default_display_menu': True}</field>
</record>
<menuitem
action="action_stock_barcodes"
id="menu_action_stock_barcodes"
groups="stock.group_stock_user"
name="Barcode actions"
parent="stock.menu_product_in_config_stock"
sequence="100"
/>
</odoo>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<menuitem
id="stock_barcodes_root"
name="Barcodes"
web_icon="stock_barcodes,static/description/icon.png"
action="stock_barcodes.action_stock_barcodes_action_client"
groups="stock.group_stock_user"
/>
</odoo>

Some files were not shown because too many files have changed in this diff Show more