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,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>

View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_option_group_form" model="ir.ui.view">
<field name="name">stock.barcodes.option.group.form</field>
<field name="model">stock.barcodes.option.group</field>
<field name="arch" type="xml">
<form string="Barcode options">
<sheet>
<group>
<group>
<field name="name" />
<field name="barcode_guided_mode" />
</group>
<group>
<field name="code" />
<field
name="display_notification"
widget="boolean_toggle"
/>
</group>
</group>
<group string="Behavior settings">
<group>
<field name="manual_entry" />
<field name="manual_entry_field_focus" />
<field name="confirmed_moves" />
<field name="show_pending_moves" />
<field
name="source_pending_moves"
attrs="{'invisible': [('show_pending_moves', '=', False), ('barcode_guided_mode', '=', False)]}"
/>
<field name="group_key_for_todo_records" />
<field name="location_field_to_sort" />
<field name="show_detailed_operations" />
<field name="auto_put_in_pack" />
<field name="display_read_quant" />
</group>
<group>
<field name="ignore_filled_fields" />
<field name="keep_screen_values" />
<field name="is_manual_confirm" />
<field name="is_manual_qty" />
<field name="accumulate_read_quantity" />
<field name="allow_negative_quant" />
<field name="fill_fields_from_lot" />
<field name="ignore_quant_location" />
<field name="auto_lot" />
<field name="create_lot" />
<field name="use_location_dest_putaway" />
</group>
</group>
<separator string="Steps to scan" />
<field name="option_ids">
<tree editable="bottom">
<field name="step" />
<field name="sequence" widget="handle" />
<field name="name" />
<field name="field_name" />
<field name="filled_default" />
<field
name="forced"
attrs="{'column_invisible': [('parent.barcode_guided_mode', '=', False)]}"
/>
<field name="to_scan" />
<field name="required" />
<field name="clean_after_done" />
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record id="view_stock_barcodes_option_tree" model="ir.ui.view">
<field name="name">stock.barcodes.option.tree</field>
<field name="model">stock.barcodes.option</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="sequence" />
<field name="name" />
<field
name="option_group_id"
invisible="context.get('hide_option_group_id', False)"
/>
<field name="field_name" />
<field name="filled_default" />
<field name="forced" />
<field name="to_scan" />
<field name="required" />
<field name="clean_after_done" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes_option">
<field name="res_model">stock.barcodes.option</field>
<field name="name">Barcodes options</field>
<field name="view_mode">tree</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes_option_group">
<field name="res_model">stock.barcodes.option.group</field>
<field name="name">Barcodes options groups</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_action_stock_barcodes_option_group"
action="action_stock_barcodes_option_group"
groups="stock.group_stock_user"
name="Barcode options"
parent="stock.menu_product_in_config_stock"
sequence="100"
/>
</odoo>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!--
Odoo v13.0 has been removed barcode field from location view, so we need to
add the field to allow to assign a value
-->
<record id="stock_location_form_inherit" model="ir.ui.view">
<field name="name">stock.location.inherit</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form" />
<field name="arch" type="xml">
<group name="additional_info" position="after">
<group name="barcode" string="Barcode">
<field name="barcode" />
</group>
</group>
</field>
</record>
</odoo>

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_picking_form" model="ir.ui.view">
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
name="action_barcode_scan"
class="oe_stat_button"
icon="fa-barcode"
type="object"
help="Start barcode interface"
states="confirmed,assigned"
>
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Scan barcodes</span>
</div>
</button>
</div>
</field>
</record>
<record id="stock_picking_kanban" model="ir.ui.view">
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.stock_picking_kanban" />
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="type">object</attribute>
</xpath>
<xpath expr="//kanban" position="attributes">
<attribute name="action">action_barcode_scan</attribute>
</xpath>
<xpath expr="//kanban" position="attributes">
<attribute name="js_class">stock_barcodes_kanban</attribute>
</xpath>
</field>
</record>
<record id="stock_picking_type_kanban" model="ir.ui.view">
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.stock_picking_type_kanban" />
<field name="arch" type="xml">
<field name="code" position="after">
<field name="default_location_src_id" invisible="1" />
<field name="default_location_dest_id" invisible="1" />
</field>
<xpath expr="//div[hasclass('o_kanban_primary_left')]" position="inside">
<field name="barcode_option_group_id" invisible="1" />
<div
class="mt8"
t-if="['incoming', 'outgoing', 'internal'].indexOf(record.code.raw_value) &gt; -1"
attrs="{'invisible': [('barcode_option_group_id', '=', False)]}"
>
<button
name="action_barcode_scan"
type="object"
class="btn btn-secondary"
>
<i
class="fa fa-barcode fa-2x"
title="Start barcode interface"
/>
</button>
</div>
</xpath>
<xpath expr="//div[hasclass('o_kanban_primary_right')]" position="inside">
<div
class="float-end mt8"
t-if="record.default_location_src_id.raw_value and record.default_location_dest_id.raw_value"
>
<button
name="action_barcode_new_picking"
type="object"
class="btn btn-secondary"
context="{'default_immediate_transfer': True, 'manual_picking': True}"
>
<i class="fa fa-plus" title="Scan new picking " />
<small>New</small>
</button>
</div>
</xpath>
</field>
</record>
<record id="view_picking_type_form" model="ir.ui.view">
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_picking_type_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='use_create_lots']/../.." position="after">
<group string="Barcodes">
<field name="barcode_option_group_id" />
<field name="new_picking_barcode_option_group_id" />
</group>
</xpath>
</field>
</record>
<record id="view_pickingtype_filter" model="ir.ui.view">
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_pickingtype_filter" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="code" string="Type of operation" />
<field name="sequence_code" string="Code" />
<filter
string="Barcode Options"
name="barcode_options"
domain="[('barcode_option_group_id','!=',False)]"
/>
</field>
</field>
</record>
<record id="stock_barcodes_action_picking_tree_ready" model="ir.actions.act_window">
<field name="name">Operations</field>
<field name="res_model">stock.picking</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">kanban</field>
<field
name="context"
>{'contact_display': 'partner_address', 'search_default_available': 1}
</field>
<field name="search_view_id" ref="stock.view_picking_internal_search" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No transfer found. Let's create one!
</p>
<p>
Transfers allow you to move products from one location to another.
</p>
</field>
</record>
</odoo>

View file

@ -0,0 +1,6 @@
from . import stock_barcodes_read
from . import stock_barcodes_read_inventory
from . import stock_barcodes_candidate_picking
from . import stock_barcodes_read_picking
from . import stock_barcodes_read_todo
from . import stock_production_lot

View file

@ -0,0 +1,148 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).f
from odoo import api, fields, models
class WizCandidatePicking(models.TransientModel):
_name = "wiz.candidate.picking"
_description = "Candidate pickings for barcode interface"
# To prevent remove the record wizard until 2 days old
_transient_max_hours = 48
wiz_barcode_id = fields.Many2one(
comodel_name="wiz.stock.barcodes.read.picking", readonly=True
)
picking_id = fields.Many2one(
comodel_name="stock.picking", string="Picking", readonly=True
)
wiz_picking_id = fields.Many2one(
comodel_name="stock.picking",
related="wiz_barcode_id.picking_id",
string="Wizard Picking",
readonly=True,
)
name = fields.Char(
related="picking_id.name", readonly=True, string="Candidate Picking"
)
partner_id = fields.Many2one(
comodel_name="res.partner",
related="picking_id.partner_id",
readonly=True,
string="Partner",
)
state = fields.Selection(related="picking_id.state", readonly=True)
date = fields.Datetime(
related="picking_id.date", readonly=True, string="Creation Date"
)
product_qty_reserved = fields.Float(
"Reserved",
compute="_compute_picking_quantity",
digits="Product Unit of Measure",
readonly=True,
)
product_uom_qty = fields.Float(
"Demand",
compute="_compute_picking_quantity",
digits="Product Unit of Measure",
readonly=True,
)
product_qty_done = fields.Float(
"Done",
compute="_compute_picking_quantity",
digits="Product Unit of Measure",
readonly=True,
)
# For reload kanban view
scan_count = fields.Integer()
is_pending = fields.Boolean(compute="_compute_is_pending")
note = fields.Html(related="picking_id.note")
@api.depends("scan_count")
def _compute_picking_quantity(self):
for candidate in self:
qty_reserved = 0
qty_demand = 0
qty_done = 0
candidate.product_qty_reserved = sum(
candidate.picking_id.mapped("move_ids.reserved_availability")
)
for move in candidate.picking_id.move_ids:
qty_reserved += move.reserved_availability
qty_demand += move.product_uom_qty
qty_done += move.quantity_done
candidate.update(
{
"product_qty_reserved": qty_reserved,
"product_uom_qty": qty_demand,
"product_qty_done": qty_done,
}
)
@api.depends("scan_count")
def _compute_is_pending(self):
for rec in self:
rec.is_pending = bool(rec.wiz_barcode_id.pending_move_ids)
def _get_wizard_barcode_read(self):
return self.env["wiz.stock.barcodes.read.picking"].browse(
self.env.context["wiz_barcode_id"]
)
def action_lock_picking(self):
wiz = self._get_wizard_barcode_read()
picking_id = self.env.context["picking_id"]
wiz.picking_id = picking_id
wiz._set_candidate_pickings(wiz.picking_id)
return wiz.action_confirm()
def action_unlock_picking(self):
wiz = self._get_wizard_barcode_read()
wiz.update(
{
"picking_id": False,
"candidate_picking_ids": False,
"message_type": False,
"message": False,
}
)
return wiz.action_cancel()
def _get_picking_to_validate(self):
"""Inject context show_picking_type_action_tree to redirect to picking list
after validate picking in barcodes environment.
The stock_barcodes_validate_picking key allows to know when a picking has been
validated from stock barcodes interface.
"""
return (
self.env["stock.picking"]
.browse(self.env.context.get("picking_id", False))
.with_context(
show_picking_type_action_tree=True, stock_barcodes_validate_picking=True
)
)
def action_validate_picking(self):
context = dict(self.env.context)
picking = self._get_picking_to_validate()
if picking._check_immediate():
return False, picking.with_context(
button_validate_picking_ids=picking.ids, operations_mode=True
)._action_generate_immediate_wizard(
show_transfers=picking._should_show_transfers()
)
return (
True,
picking.with_context(
skip_sms=context.get("skip_sms", False)
).button_validate(),
)
def action_open_picking(self):
picking = self.env["stock.picking"].browse(
self.env.context.get("picking_id", False)
)
return picking.with_context(control_panel_hidden=False).get_formview_action()
def action_put_in_pack(self):
self.picking_id.action_put_in_pack()

View file

@ -0,0 +1,905 @@
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
TYPE_ERROR = ["more_match", "not_found"]
class WizStockBarcodesRead(models.AbstractModel):
_name = "wiz.stock.barcodes.read"
_inherit = "barcodes.barcode_events_mixin"
_description = "Wizard to read barcode"
# To prevent remove the record wizard until 2 days old
_transient_max_hours = 48
_allowed_product_types = ["product", "consu"]
_rec_name = "barcode"
barcode = fields.Char()
res_model_id = fields.Many2one(comodel_name="ir.model", index=True)
res_id = fields.Integer(index=True)
product_id = fields.Many2one(
comodel_name="product.product", domain=[("type", "in", _allowed_product_types)]
)
product_uom_id = fields.Many2one(comodel_name="uom.uom")
product_tracking = fields.Selection(related="product_id.tracking", readonly=True)
lot_id = fields.Many2one(comodel_name="stock.lot")
lot_name = fields.Char(
"Lot/Serial Number Name",
compute="_compute_lot_name",
readonly=False,
store=True,
)
location_id = fields.Many2one(comodel_name="stock.location")
location_dest_id = fields.Many2one(
comodel_name="stock.location", string="Location dest."
)
packaging_id = fields.Many2one(comodel_name="product.packaging")
product_packaging_ids = fields.One2many(related="product_id.packaging_ids")
package_id = fields.Many2one(comodel_name="stock.quant.package")
result_package_id = fields.Many2one(comodel_name="stock.quant.package")
owner_id = fields.Many2one(comodel_name="res.partner")
packaging_qty = fields.Float(string="Package Qty", digits="Product Unit of Measure")
product_qty = fields.Float(digits="Product Unit of Measure")
manual_entry = fields.Boolean(string="Manual", help="Entry manual data")
confirmed_moves = fields.Boolean(
string="Confirmed moves", related="option_group_id.confirmed_moves"
)
message_type = fields.Selection(
[
("info", "Barcode read with additional info"),
("info_page", "Info page"),
("not_found", "No barcode found"),
("more_match", "More than one matches found"),
("success", "Barcode read correctly"),
],
readonly=True,
)
message = fields.Char(readonly=True)
message_step = fields.Char(readonly=True)
guided_product_id = fields.Many2one(comodel_name="product.product")
guided_location_id = fields.Many2one(comodel_name="stock.location")
guided_location_dest_id = fields.Many2one(comodel_name="stock.location")
guided_lot_id = fields.Many2one(comodel_name="stock.lot")
action_ids = fields.Many2many(
comodel_name="stock.barcodes.action", compute="_compute_action_ids"
)
option_group_id = fields.Many2one(comodel_name="stock.barcodes.option.group")
visible_force_done = fields.Boolean()
step = fields.Integer()
is_manual_qty = fields.Boolean(compute="_compute_is_manual_qty")
is_manual_confirm = fields.Boolean(compute="_compute_is_manual_qty")
# Technical field to allow use in attrs
display_menu = fields.Boolean()
auto_lot = fields.Boolean(
string="Get lots automatically",
help="If checked the lot will be set automatically with the same "
"removal startegy",
compute="_compute_auto_lot",
store=True,
readonly=False,
)
create_lot = fields.Boolean(
string="Allow create lot",
help="Show lot name field",
compute="_compute_create_lot",
)
display_assign_serial = fields.Boolean(compute="_compute_display_assign_serial")
keep_result_package = fields.Boolean()
total_product_uom_qty = fields.Float(
string="Product Demand", digits="Product Unit of Measure", store=False
)
total_product_qty_done = fields.Float(
string="Product Qty. Done", digits="Product Unit of Measure", store=False
)
enable_add_product = fields.Boolean(default=True)
@api.depends("res_id")
def _compute_action_ids(self):
actions = self.env["stock.barcodes.action"].search(
[("action_window_id", "!=", False)]
)
self.action_ids = actions
@api.depends("option_group_id")
def _compute_is_manual_qty(self):
for rec in self:
rec.is_manual_qty = rec.option_group_id.is_manual_qty
rec.is_manual_confirm = rec.option_group_id.is_manual_confirm
rec.auto_lot = rec.option_group_id.auto_lot
@api.depends("option_group_id")
def _compute_auto_lot(self):
for rec in self:
rec.auto_lot = rec.option_group_id.auto_lot
@api.depends("option_group_id")
def _compute_create_lot(self):
for rec in self:
rec.create_lot = rec.option_group_id.create_lot
@api.depends("product_id")
def _compute_display_assign_serial(self):
for rec in self:
rec.display_assign_serial = rec.product_id.tracking == "serial"
@api.depends("lot_id")
def _compute_lot_name(self):
for rec in self:
rec.lot_name = rec.lot_id.name
@api.onchange("packaging_qty")
def onchange_packaging_qty(self):
if self.packaging_id:
self.product_qty = self.packaging_qty * self.packaging_id.qty
@api.onchange(
"product_id",
"lot_id",
"package_id",
"result_package_id",
"packaging_qty",
"product_qty",
)
def onchange_visible_force_done(self):
self.visible_force_done = False
def _set_messagge_info(self, message_type, message):
"""
Set message type and message description.
For manual entry mode barcode is not set so is not displayed
"""
self.message_type = message_type
# if self.barcode and self.message_type in ["more_match", "not_found"]:
if self.barcode:
self.message = _(
"%(barcode)s (%(message)s)", barcode=self.barcode, message=message
)
else:
if message_type in TYPE_ERROR:
self.manual_entry = True
self.send_bus_done(
"stock_barcodes_scan",
"actions_barcode_notification",
{
"message": message,
"sticky": True,
"message_type": "danger"
if message_type in TYPE_ERROR
else message_type,
},
)
elif message_type != "info_page":
self.send_bus_done(
"stock_barcodes_scan",
"actions_barcode_notification",
{
"message": message,
"message_type": message_type,
},
)
else:
self.message = "%s" % message
def process_barcode_location_id(self):
location = self.env["stock.location"].search(self._barcode_domain(self.barcode))
if location:
self.location_id = location
return True
return False
def process_barcode_location_dest_id(self):
location = self.env["stock.location"].search(self._barcode_domain(self.barcode))
if location:
self.location_dest_id = location
return True
return False
def process_barcode_product_id(self):
domain = self._barcode_domain(self.barcode)
product = self.env["product.product"].search(domain)
if product:
if len(product) > 1:
self._set_messagge_info("more_match", _("More than one product found"))
return False
elif product.type not in self._allowed_product_types:
self._set_messagge_info(
"not_found", _("The product type is not allowed")
)
return False
self.action_product_scaned_post(product)
if (
self.option_group_id.fill_fields_from_lot
and self.location_id
and self.product_id
):
quant_domain = [
("location_id", "=", self.location_id.id),
("product_id", "=", product.id),
]
if self.lot_id:
quant_domain.append(("lot_id", "=", self.lot_id.id))
if self.package_id:
quant_domain.append(("package_id", "=", self.package_id.id))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
if quants:
self.set_info_from_quants(quants)
return True
return False
def process_barcode_lot_id(self):
if self.env.user.has_group("stock.group_production_lot"):
lot_domain = [("name", "=", self.barcode)]
if self.product_id:
lot_domain.append(("product_id", "=", self.product_id.id))
lot = self.env["stock.lot"].search(lot_domain)
if len(lot) == 1:
if self.option_group_id.fill_fields_from_lot:
quant_domain = [
("lot_id.name", "=", self.barcode),
("product_id", "=", lot.product_id.id),
("quantity", ">", 0.0),
]
if self.location_id:
quant_domain.append(("location_id", "=", self.location_id.id))
else:
quant_domain.append(("location_id.usage", "=", "internal"))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
if (
not self._name == "wiz.stock.barcodes.read.inventory"
and not quants
and not self.option_group_id.allow_negative_quant
):
self._set_messagge_info(
"more_match",
_("No stock available for this lot with screen values"),
)
self.lot_id = False
self.lot_name = False
return False
if quants:
self.set_info_from_quants(quants)
else:
self.product_id = lot.product_id
self.action_lot_scaned_post(lot)
return True
else:
self.product_id = lot.product_id
self.action_lot_scaned_post(lot)
return True
elif lot:
self._set_messagge_info(
"more_match", _("More than one lot found\nScan product before")
)
elif (
self.product_id
and self.product_id.tracking != "none"
and self.option_group_id.create_lot
):
self.lot_name = self.barcode
self.action_lot_scaned_post(self.lot_name)
return True
return False
def process_barcode_package_id(self):
if not self.env.user.has_group("stock.group_tracking_lot"):
return False
quant_domain = [
("package_id.name", "=", self.barcode),
("quantity", ">", 0.0),
]
if self.option_group_id.get_option_value("location_id", "forced"):
quant_domain.append(("location_id", "=", self.location_id.id))
if self.owner_id:
quant_domain.append(("owner_id", "=", self.owner_id.id))
quants = self.env["stock.quant"].search(quant_domain)
internal_quants = quants.filtered(lambda q: q.location_id.usage == "internal")
if internal_quants:
quants = internal_quants
elif quants:
self = self.with_context(ignore_quant_location=True)
# self._set_messagge_info("more_match", _("Package located external location"))
else:
# self._set_messagge_info("more_match", _("Package not fount or empty"))
return False
self.set_info_from_quants(quants)
return True
def process_barcode_result_package_id(self):
if not self.env.user.has_group("stock.group_tracking_lot"):
return False
domain = [("name", "=", self.barcode)]
package = self.env["stock.quant.package"].search(domain)
if package:
self.result_package_id = package[:1]
return True
return False
def set_info_from_quants(self, quants):
"""
Fill wizard fields from stock quants
"""
if self.env.context.get("skip_set_info_from_quants"):
return
ignore_quant_location = self.env.context.get(
"ignore_quant_location", self.option_group_id.ignore_quant_location
)
if len(quants) == 1:
# All ok
self.action_product_scaned_post(quants.product_id)
self.package_id = quants.package_id
self.result_package_id = quants.package_id
if quants.lot_id:
self.action_lot_scaned_post(quants.lot_id)
if quants.owner_id:
self.owner_id = quants.owner_id
# Review conditions
if (
not ignore_quant_location
and not self.option_group_id.get_option_value("location_id", "forced")
and self.option_group_id.code != "IN"
):
self.location_id = quants.location_id
if self.option_group_id.code != "OUT" and not self.env.context.get(
"skip_update_quantity_from_lot", False
):
self.product_qty = quants.quantity
elif len(quants) > 1:
# More than one record found with same barcode.
# Could be half lot in two distinct locations.
# Empty location field to force a location barcode scan
products = quants.mapped("product_id")
if len(products) == 1:
self.action_product_scaned_post(products[0])
package = quants[0].package_id
if not quants.filtered(lambda q: q.package_id != package):
self.package_id = package
lots = quants.mapped("lot_id")
if len(lots) == 1:
self.action_lot_scaned_post(lots[0])
owner = quants[0].owner_id
if not quants.filtered(lambda q: q.owner_id != owner):
self.owner_id = owner
if not ignore_quant_location:
locations = quants.mapped("location_id")
if len(locations) == 1:
if not self.location_id and self.option_group_id.code != "IN":
self.location_id = locations
def process_barcode_packaging_id(self):
domain = self._barcode_domain(self.barcode)
if self.env.user.has_group("product.group_stock_packaging"):
domain.append(("product_id", "!=", False))
packaging = self.env["product.packaging"].search(domain)
if packaging:
if len(packaging) > 1:
self._set_messagge_info(
"more_match", _("More than one package found")
)
self.packaging_id = False
return False
self.action_packaging_scaned_post(packaging)
return True
return False
def process_barcode(self, barcode):
if not self:
barcode_action = self.env["stock.barcodes.action"].search(
[
("action_window_id", "!=", False),
("barcode", "=", barcode),
],
limit=1,
)
self.env["bus.bus"]._sendone(
"stock_barcodes_scan",
"actions_main_menu_barcode",
{
"action_ok": len(barcode_action) > 0,
"action": barcode_action.open_action() if barcode_action else "",
"barcode": barcode,
},
)
else:
self._set_messagge_info("success", _("OK"))
options = self.option_group_id.option_ids
barcode_found = False
options_to_scan = options.filtered("to_scan")
options_required = options.filtered("required")
options_to_scan = options_to_scan.filtered(lambda op: op.step == self.step)
for option in options_to_scan:
if (
self.option_group_id.ignore_filled_fields
and option in options_required
and getattr(self, option.field_name, False)
):
continue
option_func = getattr(
self, "process_barcode_%s" % option.field_name, False
)
if option_func:
res = option_func()
if res:
barcode_found = True
self.play_sounds(barcode_found)
break
elif self.message_type != "success":
self.play_sounds(False)
return False
if not barcode_found:
self.play_sounds(barcode_found)
if self.option_group_id.ignore_filled_fields:
self._set_messagge_info(
"not_found", _("Barcode not found or field already filled")
)
else:
self._set_messagge_info(
"not_found", _("Barcode not found with this screen values")
)
self.display_notification(
self.barcode,
message_type="danger",
title=_("Barcode not found"),
sticky=False,
)
return False
if not self.check_option_required():
return False
if self.is_manual_confirm or self.manual_entry:
self._set_messagge_info("info", _("Review and confirm"))
return False
return self.action_confirm()
def check_option_required(self):
options = self.option_group_id.option_ids
options_required = options.filtered("required")
for option in options_required:
if not getattr(self, option.field_name, False):
if self.is_manual_qty and option.field_name in [
"product_qty",
"packaging_qty",
]:
self._set_focus_on_qty_input("product_qty")
if option.field_name == "lot_id" and (
self.product_id.tracking == "none"
or self.auto_lot
or (self.lot_name and self.create_lot)
):
continue
if self._option_required_hook(option):
continue
self.display_notification(
_("{name} is required").format(name=option.name),
message_type="danger",
title=_("Empty field"),
sticky=False,
)
self.action_show_step()
return False
return True
def _option_required_hook(self, option_required):
"""Hook to evaluate is an option is required"""
return False
def _scanned_location(self, barcode):
location = self.env["stock.location"].search(self._barcode_domain(barcode))
if location:
self.location_id = location
self._set_messagge_info("info", _("Waiting product"))
return True
else:
return False
def _barcode_domain(self, barcode):
field_name = self.env.context.get("barcode_domain_field", "barcode")
return [(field_name, "=", barcode)]
def _clean_barcode_scanned(self, barcode):
return barcode.rstrip()
def on_barcode_scanned(self, barcode):
self.barcode = self._clean_barcode_scanned(barcode)
def dummy_on_barcode_scanned(self):
"""To avoid execute operations in onchange environment"""
self.process_barcode(self.barcode)
def check_location_contidion(self):
if not self.location_id:
self._set_messagge_info("info", _("Waiting location"))
# Remove product when no location has been scanned
self.product_id = False
return False
return True
def check_lot_contidion(self):
if self.product_id.tracking != "none" and not self.lot_id and not self.lot_name:
self._set_messagge_info("info", _("Waiting lot"))
return False
return True
def check_done_conditions(self):
result_ok = self.check_location_contidion()
if not result_ok:
return False
if not self.product_id:
self._set_messagge_info("info", _("Waiting product"))
return False
result_ok = self.check_lot_contidion()
if not result_ok:
return False
if (
not self.product_qty
and not self._name == "wiz.stock.barcodes.read.inventory"
):
self._set_messagge_info("info", _("Waiting quantities"))
return False
if (
self.option_group_id.barcode_guided_mode == "guided"
and not self._check_guided_values()
):
return False
if self.manual_entry:
self._set_messagge_info("success", _("Manual entry OK"))
return True
def _check_guided_values(self):
if (
self.product_id != self.guided_product_id
and self.option_group_id.get_option_value("product_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong product"))
self.product_qty = 0.0
return False
if (
self.guided_product_id.tracking != "none"
and self.lot_id != self.guided_lot_id
and self.option_group_id.get_option_value("lot_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong lot"))
return False
if (
self.location_id != self.guided_location_id
and self.option_group_id.get_option_value("location_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong location"))
return False
if (
self.location_dest_id != self.guided_location_dest_id
and self.option_group_id.get_option_value("location_dest_id", "forced")
):
self._set_messagge_info("more_match", _("Wrong location dest"))
return False
return True
def action_done(self):
if not self.manual_entry and not self.product_qty and not self.is_manual_qty:
self.product_qty = 1.0
limit_product_qty = float(
self.env["ir.config_parameter"]
.sudo()
.get_param("stock_barcodes.limit_product_qty", "999999")
)
if self.product_qty > limit_product_qty:
# HACK: Some times users scan a barcode into input element.
# At this time, to prevent this we check that the quantity be realistic.
self._set_messagge_info("more_match", _("The quantity is huge"))
return False
if not self.check_done_conditions():
return False
self.process_lot_before_done()
return True
def action_cancel(self):
return True
def action_product_scaned_post(self, product):
self.package_id = False
if self.product_id != product and self.lot_id.product_id != product:
self.lot_id = False
self.product_id = product
self.product_uom_id = self.product_id.uom_id
self.set_product_qty()
def action_packaging_scaned_post(self, packaging):
self.packaging_id = packaging
if (
self.product_id != packaging.product_id
and self.lot_id.product_id != packaging.product_id
):
self.lot_id = False
self.product_id = packaging.product_id
self.set_product_qty()
def action_lot_scaned_post(self, lot):
if isinstance(lot, str):
self.lot_name = lot
else:
self.lot_id = lot
self.set_product_qty()
def set_product_qty(self):
if (
self.manual_entry
or self.is_manual_qty
or self.option_group_id.get_option_value("product_qty", "filled_default")
):
return
elif self.packaging_id:
self.packaging_qty = 1.0
self.product_qty = self.packaging_id.qty * self.packaging_qty
else:
self.packaging_qty = 0.0
self.product_qty = 1.0
def action_clean_lot(self):
self.lot_id = False
self.lot_name = False
self.action_show_step()
def action_clean_product(self):
self.product_id = False
self.action_show_step()
def action_clean_package(self):
self.package_id = False
self.result_package_id = False
self.action_show_step()
def action_create_package(self):
self.result_package_id = self.env["stock.quant.package"].create({})
def action_clean_values(self):
options = self.option_group_id.option_ids
options_to_clean = options.filtered(
lambda op: op.clean_after_done and op.field_name in self
)
for option in options_to_clean:
if option.field_name == "result_package_id" and self.keep_result_package:
continue
if option.field_name:
setattr(self, option.field_name, False)
self.action_show_step()
self.product_qty = 0.0
self.packaging_qty = 0.0
self.lot_name = False
def action_manual_entry(self):
return True
def reset_qty(self):
self.product_qty = 0
self.packaging_qty = 0
def open_actions(self):
self.display_menu = True
return self.env.ref(
"stock_barcodes.action_stock_barcodes_action_client"
).read()[0]
def action_back(self):
return self.env.ref("stock.stock_picking_type_action").read()[0]
def open_records(self):
action = self.action_ids
return action
def get_option_value(self, field_name, attribute):
option = self.option_group_id.option_ids.filtered(
lambda op: op.field_name == field_name
)[:1]
return option[attribute]
def action_force_done(self):
res = self.with_context(force_create_move=True).action_confirm()
self.visible_force_done = False
return res
@api.model_create_multi
def create(self, vals_list):
wizards = super().create(vals_list)
for wiz in wizards:
wiz.action_show_step()
return wizards
def action_manual_quantity(self):
action = self.get_formview_action()
form_view = self.env.ref(
"stock_barcodes.view_stock_barcodes_read_form_manual_qty"
)
action["views"] = [(form_view.id, "form")]
action["res_id"] = self.ids[0]
return action
def action_reopen_wizard(self):
return self.get_formview_action()
@api.onchange("step")
def action_show_step(self):
options_required = self.option_group_id.option_ids.filtered("required")
self.step = 0
for option in options_required:
if not getattr(self, option.field_name, False):
if option.field_name == "lot_id" and self.product_id.tracking == "none":
continue
self.step = option.step
break
if not self.step:
self.step = options_required[:1].step
options = self.option_group_id.option_ids.filtered(
lambda op: op.step == self.step and op.to_scan
)
self._set_messagge_info(
"info_page", _("Scan {}").format(", ".join(options.mapped("name")))
)
@api.onchange("package_id")
def onchange_package_id(self):
if self.manual_entry:
self.barcode = self.package_id.name
self.process_barcode_package_id()
def action_confirm(self):
if not self.check_option_required():
self.play_sounds(False)
return False
record = self.browse(self.ids)
record.write(self._convert_to_write(self._cache))
self = record
no_increase_qty_done, force_create_move = False, False
context = dict(self.env.context)
if self._name == "wiz.stock.barcodes.read.picking":
no_increase_qty_done = (
context.get("no_increase_qty_done", False) or self.manual_entry
)
force_create_move = context.get("force_create_move", False)
res = self.with_context(
no_increase_qty_done=no_increase_qty_done,
force_create_move=force_create_move,
).action_done()
self.invalidate_recordset()
self.play_sounds(res)
self._set_focus_on_qty_input()
if force_create_move:
# Hide Form Edit
self.manual_entry = False
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": False,
},
)
# Count elements for apply in inventory
if self._name == "wiz.stock.barcodes.read.inventory":
self.display_read_quant = True
self._compute_count_inventory_quants()
self.send_bus_done(
"stock_barcodes_form_update",
"count_apply_inventory",
{"count": self.count_inventory_quants},
)
return res
def action_add_scan_manual(self):
self.manual_entry = True
self.send_bus_done(
"stock_barcodes_scan", "stock_barcodes_edit_manual", {"manual_entry": True}
)
def process_lot_before_done(self):
if (
not self.lot_id
and self.lot_name
and self.product_id
and self.product_id.tracking != "none"
and self.option_group_id.create_lot
):
self.lot_id = self._create_new_lot()
return True
def play_sounds(self, res):
if res:
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_sound",
{"sound": "ok", "res_model": self._name, "res_id": self.ids[0]},
)
else:
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_sound",
{"sound": "ko", "res_model": self._name, "res_id": self.ids[0]},
)
def _set_focus_on_qty_input(self, field_name=None):
if field_name is None:
field_name = "product_qty"
if field_name == "product_qty" and self.packaging_id:
field_name = "packaging_qty"
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_focus",
{
"action": "focus",
"field_name": field_name,
"res_model": self._name,
"res_id": self.ids[0],
},
)
@api.onchange("product_id")
def onchange_product_id(self):
self.product_uom_id = self.product_id.uom_id
@api.onchange("manual_entry")
def onchange_manual_entry(self):
if self.manual_entry and self.option_group_id.manual_entry_field_focus:
self._set_focus_on_qty_input(self.option_group_id.manual_entry_field_focus)
def _prepare_lot_vals(self):
return {
"name": self.lot_name,
"product_id": self.product_id.id,
"company_id": self.env.company.id,
}
def _create_new_lot(self):
StockProductionLot = self.env["stock.lot"]
lot_domain = [
("name", "=", self.lot_name),
("product_id", "=", self.product_id.id),
]
new_lot = StockProductionLot.search(lot_domain)
if not new_lot:
new_lot = StockProductionLot.create(self._prepare_lot_vals())
return new_lot
def action_clean_message(self):
self.message = False
self.check_option_required()
def action_keep_result_package(self):
self.keep_result_package = not self.keep_result_package
def display_notification(
self, message, message_type="warning", title=False, sticky=True
):
"""Send notifications to web client
message_type:
[options.type='warning'] 'info', 'success', 'warning', 'danger' or ''
sticky: Permanent notification until user removes it
"""
if self.option_group_id.display_notification and not self.env.context.get(
"skip_display_notification", False
):
message = {
"message": message,
"type": message_type,
"sticky": sticky,
"res_model": self._name,
"res_id": self.ids[0],
}
if title:
message["title"] = title
self.send_bus_done(
"stock_barcodes-{}".format(self.ids[0]),
"stock_barcodes_notify-{}".format(self.ids[0]),
message,
)

View file

@ -0,0 +1,162 @@
# Copyright 2023 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, fields, models
class WizStockBarcodesReadInventory(models.TransientModel):
_name = "wiz.stock.barcodes.read.inventory"
_inherit = "wiz.stock.barcodes.read"
_description = "Wizard to read barcode on inventory"
_allowed_product_types = ["product"]
# Overwrite is needed to take into account new domain values
product_id = fields.Many2one(domain=[("type", "in", _allowed_product_types)])
inventory_product_qty = fields.Float(
string="Inventory quantities", digits="Product Unit of Measure", readonly=True
)
inventory_quant_ids = fields.Many2many(
comodel_name="stock.quant", compute="_compute_inventory_quant_ids"
)
count_inventory_quants = fields.Integer(
compute="_compute_count_inventory_quants", store=True
)
display_read_quant = fields.Boolean(string="Read items", default=True)
def action_display_read_quant(self):
self.display_read_quant = not self.display_read_quant
@api.depends("inventory_quant_ids")
def _compute_count_inventory_quants(self):
for wiz in self:
wiz.count_inventory_quants = len(wiz.inventory_quant_ids)
@api.depends("display_read_quant")
def _compute_inventory_quant_ids(self):
for wiz in self:
domain = [
("user_id", "=", self.env.user.id),
("inventory_date", "<=", fields.Date.context_today(self)),
]
if wiz.display_read_quant:
domain.append(("inventory_quantity_set", "=", True))
order = "write_date DESC"
else:
domain.append(("inventory_quantity_set", "=", False))
order = None
quants = self.env["stock.quant"].search(domain, order=order)
if order is None:
quants = quants.sorted(
lambda q: (
q.location_id.posx,
q.location_id.posy,
q.location_id.posz,
q.location_id.name,
)
)
wiz.inventory_quant_ids = quants
# UPDATE: Count elements for apply in inventory
wiz.send_bus_done(
"stock_barcodes_form_update",
"count_apply_inventory",
{"count": wiz.count_inventory_quants},
)
def _prepare_stock_quant_values(self):
return {
"product_id": self.product_id.id,
"location_id": self.location_id.id,
"inventory_quantity": self.product_qty,
"lot_id": self.lot_id.id,
"package_id": self.package_id.id,
}
def _inventory_quant_domain(self):
return [
("user_id", "=", self.env.user.id),
(
"inventory_date",
"<=",
fields.Date.context_today(self).strftime("%Y-%m-%d"),
),
("product_id", "=", self.product_id.id),
("location_id", "=", self.location_id.id),
("lot_id", "=", self.lot_id.id),
("package_id", "=", self.package_id.id),
]
def _add_inventory_quant(self):
StockQuant = self.env["stock.quant"]
quant = StockQuant.search(self._inventory_quant_domain(), limit=1)
quant = quant.with_context(inventory_mode=True)
if quant:
if self.product_id.tracking == "serial" and (
quant.inventory_quantity > 0.0 or self.product_qty != 1
):
self._serial_tracking_message_fail()
return False
if self.option_group_id.accumulate_read_quantity:
quant.inventory_quantity += self.product_qty
else:
quant.inventory_quantity = self.product_qty
else:
if self.product_id.tracking == "serial" and self.product_qty != 1:
self._serial_tracking_message_fail()
return False
quant = StockQuant.with_context(inventory_mode=True).create(
self._prepare_stock_quant_values()
)
self.inventory_product_qty = quant.quantity
return True
def _serial_tracking_message_fail(self):
self._set_messagge_info(
"more_match",
_("Inventory line with more than one unit in serial tracked product"),
)
def action_done(self):
result = super().action_done()
if result:
result = self._add_inventory_quant()
if result:
self.action_clean_values()
return result
def action_manual_entry(self):
result = super().action_manual_entry()
if result:
self.action_done()
return result
def action_clean_values(self):
res = super().action_clean_values()
self.inventory_product_qty = 0.0
self.package_id = False
# Hide Form Edit
self.manual_entry = False
self.send_bus_done(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": False,
},
)
return res
@api.onchange("product_id")
def _onchange_product_id(self):
if self.product_id != self.lot_id.product_id:
self.lot_id = False
@api.onchange("lot_id")
def _onchange_lot_id(self):
if self.lot_id and not self.env.context.get("keep_auto_lot"):
self.auto_lot = False
def apply_inventory(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"stock.action_stock_inventory_adjustement_name"
)
action["context"] = {"default_quant_ids": self.inventory_quant_ids.ids}
return action

View file

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_read_inventory_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.inventory.form</field>
<field name="model">wiz.stock.barcodes.read.inventory</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="mode">primary</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<field name="product_qty" position="after">
<field name="inventory_product_qty" invisible="1" />
</field>
<!-- hide result package from inventory -->
<group name="quant_package" position="replace">
<div class="mt4">
<strong class="d-none d-sm-block">Package</strong>
<span
class="fa fa-inbox fa-2x d-sm-none oe_span_small_icon"
title="package"
/>
<field
name="package_id"
options="{'no_create': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:85%"
class="h5"
/>
</div>
</group>
<field name="location_id" position="attributes">
<attribute name="domain">[('usage', 'in', ['internal', 'transit'])]
</attribute>
</field>
<group name="scan_fields" position="after">
<group
string="Inventory quants"
attrs="{'invisible': [('inventory_quant_ids', '=', [])]}"
col="2"
class="px-3"
>
<field
name="inventory_quant_ids"
options="{'no_open': True}"
nolabel="1"
colspan="2"
mode="kanban"
>
<kanban
class="o_kanban_mobile"
js_class="stock_barcodes_kanban"
>
<field name="product_id" />
<field name="inventory_quantity" />
<field name="product_uom_id" />
<templates>
<t t-name="kanban-box">
<div t-on-click="onCustomGlobalClick">
<div class="row w-auto">
<div class="col-8 col-md-10">
<field name="product_id" class="h2" />
</div>
<div
class="col-4 col-md-2 d-flex justify-content-end align-items-center"
>
<img
t-att-src="kanban_image('product.product', 'image_128', record.product_id.raw_value)"
role="img"
t-att-title="record.product_id.value"
height="40"
width="40"
t-att-alt="record.product_id.value"
/>
<button
name="action_barcode_inventory_quant_edit"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-pencil"
title="Edit inventory quantity"
/>
</button>
</div>
</div>
<div class="row">
<div
class="col-8 col-md-10 d-flex justify-content-start align-items-center"
>
<span
t-if="record.lot_id.raw_value"
class="h3"
>Lot S/N:&#32;&#32;</span>
<field
t-if="record.lot_id.raw_value"
name="lot_id"
class="h2"
/>
</div>
<div
class="col-4 col-md-2 d-flex justify-content-end align-items-center"
>
<span class="fw-bold mx-3">
<field name="inventory_quantity" />
</span>
<button
name="action_barcode_inventory_quant_unlink"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-trash"
title="Reset inventory quantity"
/>
</button>
</div>
</div>
<div
t-attf-class="row oe_kanban_operations oe_kanban_operations-#{record.id.raw_value} d-none"
>
<div
class="d-flex justify-content-end align-items-center"
>
<button
name="operation_quantities_rest"
type="object"
class="btn btn-lg btn-op-rest text-white ms-2 ms-sm-4 mx-2"
>-1
</button>
<button
name="operation_quantities"
type="object"
class="btn btn-lg btn-op-sum text-white"
>+1
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
</group>
<xpath
expr="//div[hasclass('oe_stock_barcodes_bottombar')]//div[hasclass('dropup')]"
position="inside"
>
<field
name="display_read_quant"
widget="barcode_boolean_toggle"
class="d-none"
/>
<button
name="action_display_read_quant"
type="object"
title="Read items"
icon="fa-eye fa-2x"
attrs="{'invisible': [('display_read_quant', '=', True)]}"
/>
<button
name="action_display_read_quant"
type="object"
title="Read items"
icon="fa-eye-slash fa-2x"
attrs="{'invisible': [('display_read_quant', '=', False)]}"
/>
</xpath>
<button name="action_clean_values" position="before">
<button
name="apply_inventory"
type="object"
icon="fa-check fa-2x"
class="btn-primary w-100 oe_kanban_action_button btn-sm d-flex justify-content-center align-items-center fs-1"
attrs="{'invisible': ['|', '|',('display_menu', '=', True), ('inventory_quant_ids', '=', []), ('display_read_quant', '=', False)]}"
data-hotkey="7"
groups="stock.group_stock_manager"
>
<span class="d-none d-lg-block">Apply</span>
(<span class="count_apply_inventory" />)
</button>
</button>
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record id="action_stock_barcodes_read_inventory" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.read.inventory</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{"control_panel_hidden": True,
"form_view_initial_mode": "edit",
"inventory_mode": True,
}
</field>
<field name="view_id" ref="view_stock_barcodes_read_inventory_form" />
<field name="target">fullscreen</field>
</record>
</odoo>

View file

@ -0,0 +1,415 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_read_picking_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.picking.form</field>
<field name="model">wiz.stock.barcodes.read.picking</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='open_actions']" position="after">
<button
name="action_open_picking"
type="object"
class="btn btn-primary"
data-hotkey="'shift+o'"
>
<field name="picking_id" />
</button>
<t t-if="partner_id">
[<field
name="partner_id"
invisible="context.get('hide_partner', False)"
/>]
</t>
<field name="candidate_picking_id" invisible="1" />
<button
name="action_unlock_picking"
type="object"
title="unlock picking"
attrs="{'invisible': [('candidate_picking_id', '=', 'picking_id')]}"
class="float-end"
>
<span class="fa-stack fa-lg">
<!-- FIXME: Use fa-thumbtack fa-stack-2x on v13 with FA v5.4 -->
<i class="fa fa-thumb-tack fa-stack-1x" />
<!-- FIXME: Use fa-slash on v13 with FA v5.4 -->
<i class="fa fa-ban fa-stack-2x" />
</span>
</button>
<button
name="action_lock_picking"
type="object"
title="lock picking"
attrs="{'invisible': [('candidate_picking_id', '!=', 'picking_id')]}"
class="fa fa-thumb-tack fa-2x float-end"
/>
</xpath>
<xpath expr="//field[@name='message_type']" position="before">
<field
name="candidate_picking_ids"
attrs="{'invisible': [('candidate_picking_ids', '=', [])]}"
mode="kanban"
nolabel="1"
force_save="1"
class="o_x2m_control_panel"
options="{'always_reload': True}"
>
<kanban>
<field name="name" />
<field name="partner_id" />
<field name="date" />
<field name="state" />
<field name="picking_id" />
<field name="wiz_picking_id" />
<field name="product_qty_reserved" />
<field name="product_uom_qty" />
<field name="product_qty_done" />
<field name="scan_count" />
<field name="is_pending" />
<field name="note" />
<templates>
<t t-name="kanban-box">
<div
t-if="!widget.isHtmlEmpty(record.note.raw_value)"
t-att-class="'oe_kanban_color_alert' + (record.is_pending.raw_value == false ? ' bg-success' : '')"
>
<div class="oe_kanban_details p-2">
<field
name="scan_count"
invisible="1"
force_save="1"
/>
<div
class="fw-bold text-center text-danger fst-italic"
>
<t
t-out="record.note.value"
invisible="context.get('hide_note', False)"
/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
<field
name="todo_line_display_ids"
mode="kanban"
force_save="1"
attrs="{'invisible': [('todo_line_display_ids', '=', [])]}"
/>
</xpath>
<field name="location_id" position="before">
<field name="picking_type_code" invisible="1" force_save="1" />
<field name="picking_id" invisible="1" force_save="1" />
<field name="show_detailed_operations" invisible="1" />
<field name="picking_location_id" invisible="1" />
<field name="picking_location_dest_id" invisible="1" />
<field name="company_id" invisible="1" />
<field name="todo_line_is_extra_line" invisible="1" />
<field name="qty_available" invisible="1" />
</field>
<field name="location_id" position="attributes">
<attribute
name="domain"
>[('id', 'child_of', picking_location_id), '|', ('company_id', '=', False), ('company_id', '=',
company_id), ('usage', '!=', 'view')]
</attribute>
</field>
<group name="location" position="attributes">
<attribute
name="attrs"
>{'readonly': [('manual_entry', '=', False)], 'invisible': [('picking_type_code', '=', 'incoming')]}
</attribute>
</group>
<group name="location" position="after">
<div attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}">
<strong class=" d-none d-sm-block">Dest. Location</strong>
<span
class="fa fa-2x fa-share text-center d-sm-none oe_span_small_icon"
title="Destination Location"
/>
<field
name="location_dest_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
nolabel="1"
style="width:90%"
class="h5"
domain="[('id', 'child_of', picking_location_dest_id), '|', ('company_id', '=', False), ('company_id', '=', company_id), ('usage', '!=', 'view')]"
/>
</div>
</group>
<group name="scan_fields" position="attributes">
<!-- hide group scan_fields for extra todo lines -->
<attribute
name="attrs"
>{'invisible': [('todo_line_is_extra_line', '!=', False)]}
</attribute>
</group>
<group name="scan_fields" position="after">
<div
attrs="{'invisible': [('picking_state', '!=', 'done')]}"
class="oe_kanban_picking_done bg-dark d-flex flex-column justify-content-center align-items-center text-white"
>
<span class="fa fa-6x mb-4 fa-exclamation-triangle" />
<span class="fa fa-2x fa-exclamation-triangle">
This picking is already done
</span>
</div>
<group
string="Pending moves"
attrs="{'invisible': ['|', ('picking_state', '=', 'done'), ('pending_move_ids', '=', [])]}"
col="2"
>
<field
name="pending_move_ids"
options="{'no_open': True, 'always_reload': True}"
nolabel="1"
colspan="2"
force_save="1"
mode="kanban"
>
<kanban class="o_kanban_mobile">
<field name="id" />
<field name="state" />
<field name="product_id" />
<field name="product_uom_qty" />
<field name="qty_done" />
<field name="qty_done_rest" />
<field name="uom_id" />
<field name="product_qty_reserved" />
<field name="picking_state" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="row">
<div class="col-8 col-md-8">
<field name="product_id" class="h2" />
<div
class="d-flex justify-content-start align-items-center indent h4 pt-3"
>
<span
t-esc="record.qty_done.raw_value"
/>
&#32;/&#32;
<span
t-esc="record.product_uom_qty.raw_value"
/>
<span
t-if="record.uom_id"
t-esc="record.uom_id.value"
/>
</div>
</div>
<div
class="col-4 col-md-4 d-flex justify-content-end align-items-center"
>
<button
name="action_barcode_inventory_quant_edit"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-pencil"
title="Edit inventory quantity"
/>
</button>
<button
name="operation_quantities"
type="object"
class="btn btn-lg btn-primary btn-op-sum"
t-if="record.qty_done_rest.raw_value > 0"
>
+
<span
class="text-white"
t-esc="record.qty_done_rest.raw_value"
/>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
<h3
class="mt-4 w-100 px-4"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
>
Detailed operations
</h3>
<group
string="Detailed operations"
col="2"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
>
<field
name="move_line_ids"
options="{'no_open': True, 'always_reload': True}"
nolabel="1"
colspan="2"
force_save="1"
mode="kanban"
>
<kanban class="o_kanban_mobile">
<field name="product_id" />
<field name="qty_done" />
<field name="product_uom_id" />
<field name="lot_id" />
<field name="result_package_id" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="row">
<div class="col-8 col-md-8">
<field name="product_id" class="h2" />
<div
class="d-flex justify-content-start align-items-center indent h4 pt-3"
>
<t t-if="record.lot_id.raw_value">
Lot:
<span
t-esc="record.lot_id.raw_value"
/>
</t>
<t
t-if="record.result_package_id.raw_value"
>
Package:
<span
t-esc="record.result_package_id.raw_value"
/>
</t>
</div>
</div>
<div
class="col-4 col-md-4 d-flex justify-content-end align-items-center"
>
<span
class="h3"
t-esc="record.qty_done.raw_value"
/>
<button
name="action_barcode_detailed_operation_unlink"
type="object"
class="btn mt0"
context="{'wiz_barcode_id': parent.id}"
>
<i
class="fa fa-trash"
title="Remove"
/>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
</group>
<xpath expr="//button[@id='btn_create_lot']" position="after">
<field name="display_assign_serial" invisible="1" />
<button
name="action_assign_serial"
type="object"
string="Range"
title="Assign Serial Numbers"
attrs="{'invisible': [('display_assign_serial', '=', False)]}"
class="btn btn-secondary btn-sm"
/>
</xpath>
<xpath expr="//button[@name='action_clean_values']" position="before">
<field name="picking_state" invisible="1" />
<button
name="action_put_in_pack"
help="Put in pack"
type="object"
icon="fa-cube fa-2x"
title="Put in Pack"
attrs="{'invisible': ['|', ('picking_state', 'in', ('draft', 'done', 'cancel')), ('display_menu', '=', True)]}"
class="btn btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase
d-flex justify-content-center align-items-center fs-2"
groups="stock.group_tracking_lot"
data-hotkey="6"
>
<span class="d-none d-lg-block">Put in back</span>
</button>
<!-- t-att-class="'btn float-end' + (record.is_pending.raw_value == false ? ' btn-primary' : ' btn-secondary border')"-->
<button
name="action_validate_picking"
type="object"
icon="fa-check fa-2x"
class="btn btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase
d-flex justify-content-center align-items-center fs-2"
attrs="{'invisible': [('picking_state', 'not in', ['draft', 'assigned', 'confirmed'])]}"
confirm="Are you sure to validate the picking ?"
data-hotkey="'shift+v'"
>
<span class="d-none d-lg-block">Validate</span>
</button>
</xpath>
<xpath
expr="//div[hasclass('oe_stock_barcodes_bottombar')]//div[hasclass('dropup')]"
position="inside"
>
<field
name="show_detailed_operations"
widget="barcode_boolean_toggle"
class="d-none"
/>
<button
name="action_show_detailed_operations"
type="object"
title="Detailed operations"
icon="fa-eye fa-2x"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', True)]}"
/>
<button
name="action_show_detailed_operations"
type="object"
title="Detailed operations"
icon="fa-eye-slash fa-2x"
attrs="{'invisible': ['|', ('move_line_ids', '=', []), ('show_detailed_operations', '=', False)]}"
/>
</xpath>
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record model="ir.actions.act_window" id="action_stock_barcodes_read_picking">
<field name="res_model">wiz.stock.barcodes.read.picking</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{"control_panel_hidden": True,
"form_view_initial_mode": "edit"}
</field>
<field name="view_id" ref="view_stock_barcodes_read_picking_form" />
<field name="target">fullscreen</field>
</record>
<record model="ir.actions.act_window" id="action_stock_barcodes_menu">
<field name="res_model">wiz.stock.barcodes.read.picking</field>
<field name="name">Barcodes menu</field>
<field name="view_mode">form</field>
<field
name="context"
>{'control_panel_hidden': True, "default_display_menu": True}
</field>
<field name="view_id" ref="view_stock_barcodes_read_picking_form" />
<field name="target">current</field>
</record>
</odoo>

View file

@ -0,0 +1,223 @@
# 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
from odoo.tools.float_utils import float_compare
class WizStockBarcodesReadTodo(models.TransientModel):
_name = "wiz.stock.barcodes.read.todo"
_description = "Wizard to read barcode todo"
# To prevent remove the record wizard until 2 days old
_transient_max_hours = 48
name = fields.Char()
wiz_barcode_id = fields.Many2one(comodel_name="wiz.stock.barcodes.read.picking")
picking_state = fields.Selection(related="wiz_barcode_id.picking_state")
partner_id = fields.Many2one(
comodel_name="res.partner",
readonly=True,
string="Partner",
)
state = fields.Selection(
[("pending", "Pending"), ("done", "Done"), ("done_forced", "Done forced")],
string="Scan State",
default="pending",
compute="_compute_state",
readonly=False,
)
product_qty_reserved = fields.Float(
"Reserved",
digits="Product Unit of Measure",
readonly=True,
)
product_uom_qty = fields.Float(
"Demand",
digits="Product Unit of Measure",
readonly=True,
)
qty_done = fields.Float(
"Done",
digits="Product Unit of Measure",
compute="_compute_qty_done",
)
qty_done_rest = fields.Float(compute="_compute_qty_done_rest", store=True)
location_id = fields.Many2one(comodel_name="stock.location")
location_name = fields.Char(related="location_id.name")
location_dest_id = fields.Many2one(comodel_name="stock.location")
location_dest_name = fields.Char(
string="Destinatino Name", related="location_dest_id.name"
)
product_id = fields.Many2one(comodel_name="product.product")
lot_id = fields.Many2one(comodel_name="stock.lot")
uom_id = fields.Many2one(comodel_name="uom.uom")
package_id = fields.Many2one(comodel_name="stock.quant.package")
result_package_id = fields.Many2one(comodel_name="stock.quant.package")
package_product_qty = fields.Float()
res_model_id = fields.Many2one(comodel_name="ir.model")
res_ids = fields.Char()
line_ids = fields.Many2many(comodel_name="stock.move.line")
stock_move_ids = fields.Many2many(comodel_name="stock.move")
position_index = fields.Integer()
picking_code = fields.Char("Type of Operation")
is_extra_line = fields.Boolean()
# Used in kanban view
is_stock_move_line_origin = fields.Boolean()
@api.depends("qty_done", "product_uom_qty")
def _compute_qty_done_rest(self):
for rec in self:
rec.qty_done_rest = rec.product_uom_qty - rec.qty_done
def action_todo_next(self):
self.state = "done_forced"
self.line_ids.barcode_scan_state = "done_forced"
for sml in self.line_ids:
if (
float_compare(
sml.reserved_uom_qty,
sml.qty_done,
precision_rounding=sml.product_uom_id.rounding,
)
== 0
):
continue
if sml.move_id.state == "confirmed" and sml.qty_done:
sml.move_id.state = "partially_available"
if sml.move_id.state in ["partially_available", "assigned"]:
sml.reserved_uom_qty = sml.qty_done
if self.is_extra_line or not self.is_stock_move_line_origin:
barcode_backorder_action = self.env.context.get(
"barcode_backorder_action", "create_backorder"
)
self.stock_move_ids.barcode_backorder_action = barcode_backorder_action
if barcode_backorder_action == "pending":
self.stock_move_ids.move_line_ids.unlink()
self.stock_move_ids._action_assign()
wiz_barcode = self.wiz_barcode_id
self.wiz_barcode_id.fill_todo_records()
self.wiz_barcode_id = wiz_barcode
self.wiz_barcode_id.determine_todo_action()
def action_reset_lines(self):
self.state = "pending"
self.line_ids.barcode_scan_state = "pending"
self.line_ids.qty_done = 0.0
self.wiz_barcode_id.action_clean_values()
self.wiz_barcode_id.fill_todo_records()
self.wiz_barcode_id.determine_todo_action()
def action_back_line(self):
if self.position_index > 0:
record = self.wiz_barcode_id.todo_line_ids[self.position_index - 1]
self.wiz_barcode_id.determine_todo_action(forced_todo_line=record)
def action_next_line(self):
if self.position_index < len(self.wiz_barcode_id.todo_line_ids) - 1:
record = self.wiz_barcode_id.todo_line_ids[self.position_index + 1]
self.wiz_barcode_id.determine_todo_action(forced_todo_line=record)
@api.depends("line_ids.qty_done")
def _compute_qty_done(self):
for rec in self:
rec.qty_done = sum(ln.qty_done for ln in rec.line_ids)
@api.depends(
"line_ids",
"line_ids.qty_done",
"line_ids.reserved_uom_qty",
"line_ids.barcode_scan_state",
"qty_done",
"product_uom_qty",
)
def _compute_state(self):
for rec in self:
if float_compare(
rec.qty_done,
rec.product_uom_qty,
precision_rounding=rec.uom_id.rounding,
) > -1 or (
rec.wiz_barcode_id.option_group_id.source_pending_moves
== "move_line_ids"
and rec.line_ids
and (
sum(rec.stock_move_ids.mapped("quantity_done"))
>= sum(rec.stock_move_ids.mapped("product_uom_qty"))
or not any(
ln.barcode_scan_state == "pending" for ln in rec.line_ids
)
)
):
rec.state = "done"
else:
rec.state = "pending"
@api.model
def fields_to_fill_from_pending_line(self):
res = [
"location_id",
"location_dest_id",
"product_id",
"lot_id",
"package_id",
]
if not self.wiz_barcode_id.keep_result_package:
res.append("result_package_id")
return res
def fill_from_pending_line(self):
self.wiz_barcode_id.selected_pending_move_id = self
self.wiz_barcode_id.determine_todo_action(forced_todo_line=self)
for field in self.fields_to_fill_from_pending_line():
self.wiz_barcode_id[field] = self[field]
# Force fill product_qty if filled_default is set
if self.wiz_barcode_id.option_group_id.get_option_value(
"product_qty", "filled_default"
):
self.wiz_barcode_id.product_qty = self.product_uom_qty - sum(
self.line_ids.mapped("qty_done")
)
self.wiz_barcode_id.product_uom_id = self.uom_id
self.wiz_barcode_id.action_show_step()
self.wiz_barcode_id._set_focus_on_qty_input()
def operation_quantities(self):
self.wiz_barcode_id.manual_entry = True
self.wiz_barcode_id.product_qty = self.product_qty_reserved
self.wiz_barcode_id.product_id = self.product_id.id
if self.wiz_barcode_id.picking_id.picking_type_id.code != "incoming":
self.wiz_barcode_id.qty_available = self.product_qty_reserved
self.wiz_barcode_id.product_id = self.product_id.id
self.wiz_barcode_id.location_id = self.location_id.id
self.wiz_barcode_id.with_context(manual_picking=True).action_confirm()
def _get_fields_to_edit(self):
return [
"location_dest_id",
"location_id",
"product_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.picking"].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():
if hasattr(wiz_barcode, fname):
wiz_barcode[fname] = quant[fname]
wiz_barcode.product_qty = quant.qty_done
wiz_barcode.manual_entry = True
self.env["bus.bus"]._sendone(
"stock_barcodes_scan",
"stock_barcodes_edit_manual",
{
"manual_entry": True,
},
)

View file

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_todo_kanban" model="ir.ui.view">
<field name="name">stock.barcodes.todo.kanban</field>
<field name="model">wiz.stock.barcodes.read.todo</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="picking_code" />
<field name="location_id" />
<field name="location_name" />
<field name="location_dest_id" />
<field name="location_dest_name" />
<field name="product_id" />
<field name="lot_id" />
<field name="uom_id" />
<field name="package_id" />
<field name="result_package_id" />
<field name="package_product_qty" />
<field name="product_uom_qty" />
<field name="qty_done" />
<field name="line_ids" invisible="1" />
<field name="state" />
<field name="is_extra_line" />
<field name="is_stock_move_line_origin" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_content "
t-attf-style="background-color: {{record.is_extra_line.raw_value == true and '#ffd683' or '#f0f9fb'}};"
>
<div class="row">
<div class="col">
<span
class="fa fa-map-marker"
title="Location name"
/>
<strong>
<span
attrs="{'invisible': [('picking_code', '!=', 'incoming')]}"
>
<field name="location_dest_name" />
</span>
<span
attrs="{'invisible': [('picking_code', '!=', 'internal')]}"
>
<field name="location_name" /><field
name="location_dest_name"
/>
</span>
<span
attrs="{'invisible': [('picking_code', '!=', 'outgoing')]}"
>
<field name="location_name" />
</span>
</strong>
</div>
</div>
<div class="row">
<div class="col">
<strong>
<field name="product_id" />
</strong>
</div>
</div>
<t
t-if="record.lot_id or record.package_id or record.result_package_id"
>
<table>
<tr>
<td>
<span>
<span
class="fa fa-tags"
title="Lot S/N"
/>
<field
name="lot_id"
options="{'no_open': True}"
/>
</span>
</td>
<td class="text-end">
<span class="fa fa-dropbox" />
<span>
<field name="package_id" />
<t t-if="record.package_id">(<span
t-esc="record.package_product_qty.value"
/> <t
t-esc="record.uom_id.value.slice(0,3)"
/>)</t>
</span>
<span>
<field name="result_package_id" />
</span>
</td>
</tr>
</table>
</t>
<div class="row">
<div class="col-12">
<span>
<span class="fw-bold">
<t t-esc="record.qty_done.value" />
</span> / <t
t-esc="record.product_uom_qty.value"
/> <t t-esc="record.uom_id.value.slice(0,3)" />
</span>
<strong
class="bg-danger"
t-if="record.is_extra_line.raw_value == true"
>NOT AVAILABLE</strong>
</div>
</div>
<div name="action" class="row">
<div class="col-2">
<button
name="action_back_line"
type="object"
icon="fa-step-backward"
title="Previous"
class="btn-sm float-start btn btn-primary"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="1"
/>
</div>
<div class="col-4 p-0">
<button
name="action_reset_lines"
type="object"
icon="fa-trash"
title="Clean"
class="btn-sm btn mx-auto d-block btn-warning"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="2"
/>
</div>
<div class="col-4 p-0">
<button
name="action_todo_next"
type="object"
class="btn-sm mx-auto d-block btn-danger btn"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="3"
attrs="{'invisible': ['|','|', ('qty_done', '=', 0.0), ('is_extra_line', '=', True), ('is_stock_move_line_origin', '=', False)]} "
>
Ignore rest
</button>
<!-- Ask for confirmation when we've got done quantities to avoid squashing quantities -->
<button
name="action_todo_next"
type="object"
class="btn-sm mx-auto d-block btn-danger btn"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="3"
attrs="{'invisible': ['|', '|', ('qty_done', '!=', 0.0), ('is_extra_line', '=', True), ('is_stock_move_line_origin', '=', False)]} "
confirm="You have not set any quantity to this operation and it will be removed from pending moves. Are you sure?"
>Ignore rest
</button>
</div>
<div class="col-2">
<button
name="action_next_line"
type="object"
icon="fa-step-forward"
title="Next"
class="btn-sm float-end btn btn-primary"
context="{'wiz_barcode_id': parent.id}"
data-hotkey="4"
/>
</div>
</div>
<div name="action_extra" class="row mt-2">
<div class="col-12">
<button
name="action_todo_next"
type="object"
class="btn btn-warning float-end btn-sm"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'pending'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
confirm="This move will be set to pending. Are you sure?"
>
Restore to pending
</button>
<button
name="action_todo_next"
type="object"
class="btn btn-danger float-end btn-sm me-5"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'skip_backorder'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
confirm="Odoo will not create a backorder for this move. Are you sure?"
>
No Backorder
</button>
<button
name="action_todo_next"
type="object"
class="btn btn-primary float-end btn-sm me-3"
context="{'wiz_barcode_id': parent.id, 'barcode_backorder_action': 'create_backorder'}"
data-hotkey="3"
attrs="{'invisible': [('is_extra_line', '=', False), ('is_stock_move_line_origin', '=', True)]} "
>
Create Backorder
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View file

@ -0,0 +1,489 @@
<odoo>
<record id="view_stock_barcodes_read_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.form</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="arch" type="xml">
<form
string="Barcodes"
class="oe_stock_barcordes_form h-100"
js_class="stock_barcodes_form"
>
<div
class="d-flex flex-column h-100"
attrs="{'invisible': [('display_menu', '=', True)]}"
>
<div
attrs="{'invisible': [('display_menu', '=', True)]}"
class="flex-fill oe_stock_barcordes_content"
>
<div name="info" class="text-center h3 mb-2">
<div
class="alert barcode-info text-white mb-0 d-flex"
role="status"
>
<button
name="open_actions"
type="object"
class="ms-auto oe_kanban_action_button btn-sm"
title="Open actions"
attrs="{'invisible': [('display_menu', '=', True)]}"
data-hotkey="0"
>
<i class="fa fa-chevron-left fa-2x text-white" />
</button>
<field name="message" class="mt-3 h3 text-white" />
<span class="fa fa-barcode fa-2x mt-2 mx-3" />
</div>
</div>
<field name="message_type" invisible="1" />
<field name="barcode" invisible="1" force_save="1" />
<field name="step" invisible="1" force_save="1" />
<field name="is_manual_qty" invisible="1" />
<field name="is_manual_confirm" invisible="1" />
<field name="auto_lot" invisible="1" />
<field name="product_tracking" invisible="1" force_save="1" />
<field name="guided_product_id" invisible="1" force_save="1" />
<field name="guided_location_id" invisible="1" force_save="1" />
<field
name="guided_location_dest_id"
invisible="1"
force_save="1"
/>
<field name="guided_lot_id" invisible="1" force_save="1" />
<field name="visible_force_done" invisible="1" force_save="1" />
<field name="res_model_id" invisible="1" />
<field name="res_id" invisible="1" />
<field name="option_group_id" invisible="1" force_save="1" />
<field name="confirmed_moves" invisible="1" force_save="1" />
<field name="owner_id" invisible="1" force_save="1" />
<field name="keep_result_package" invisible="1" />
<field name="create_lot" invisible="1" />
<field name="lot_id" invisible="1" />
<field
name="_barcode_scanned"
widget="barcode_handler"
invisible="0"
/>
<group
name="scan_fields"
class="bg-light scan_fields p-2 d-none"
>
<group name="location" col="1">
<div class="mt-1" colspan="2">
<strong class=" d-none d-sm-block">Source Location
</strong>
<span
class="fa fa-map-marker fa-2x d-sm-none oe_span_small_icon"
title="Source Location"
/>
<field
name="location_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
nolabel="1"
style="width:90%"
class="h5"
/>
</div>
</group>
<group
name="quant_package"
groups="stock.group_tracking_lot"
col="2"
>
<div class="m-0" colspan="2">
<strong
class="d-none d-sm-block"
>Source Package -&gt; Result Package
</strong>
<span
class="fa fa-cubes d-sm-none oe_span_small_icon"
title="Source Package to Result Package"
/>
<field
name="package_id"
options="{'no_create': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
class="h5 oe_inline"
style="width: 35% !important"
/>
<span
attrs="{'invisible': [('result_package_id', '=', False)]}"
>-&gt;
</span>
<field
name="result_package_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
class="h5 oe_inline ms-1"
style="width: 35% !important"
/>
<!-- Double button to display open or closed padlock -->
<button
id="btn_keep_result_package_lock"
class="btn-sm btn-danger oe_kanban_action_button btn boder ms-1"
type="object"
name="action_keep_result_package"
title="If locked keep result package"
icon="fa-lock"
attrs="{'invisible': [('keep_result_package', '=', False)]}"
/>
<button
id="btn_keep_result_package_unlock"
class="btn-sm oe_kanban_action_button btn btn-secondary border ms-1"
type="object"
name="action_keep_result_package"
title="If locked keep result package"
icon="fa-unlock"
attrs="{'invisible': [('keep_result_package', '=', True)]}"
/>
<!-- End padlock -->
<button
class="btn-sm btn-warning oe_kanban_action_button btn border ms-1"
name="action_clean_package"
type="object"
icon="fa-trash fa-1x"
attrs="{'invisible': [('package_id', '=', False), ('result_package_id', '=', False)]}"
title="Clean package info"
/>
<button
id="btn_create_package"
class="btn-sm oe_kanban_action_button btn btn-secondary border ms-1"
type="object"
name="action_create_package"
icon="fa-plus fa-1x"
title="Create new package"
/>
</div>
</group>
<group col="2">
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Product</strong>
<span
class="fa fa-th-list fa-2x d-sm-none oe_span_small_icon"
title="Product"
/>
<field
name="product_id"
options="{'no_create': True, 'no_open': True}"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:80%"
class="h5"
/>
<button
class="btn-sm float-end btn-warning oe_kanban_action_button btn mr4"
name="action_clean_product"
type="object"
icon="fa-trash fa-1x"
title="Clean product"
attrs="{'invisible': [('product_id', '=', False)]}"
/>
</div>
</group>
<group
groups="stock.group_production_lot"
col="2"
attrs="{'invisible': [('product_tracking', 'in', [False, 'none'])]}"
>
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Lot S/N</strong>
<span
class="fa fa-tags fa-2x d-sm-none oe_span_small_icon"
title="Lot S/N"
/>
<field
name="lot_name"
attrs="{'invisible': [('create_lot', '=', False)]}"
style="width:60%"
class="h5"
/>
<field
name="lot_id"
options="{'no_create': True, 'no_open': True}"
domain="[('product_id', '=', product_id)]"
context="{'default_product_id': product_id}"
attrs="{'readonly': [('manual_entry', '=', False)], 'invisible': [('create_lot', '!=', False)]}"
force_save="1"
class="h5"
/>
<button
class="btn-sm float-end btn-warning oe_kanban_action_button btn mr4"
name="action_clean_lot"
type="object"
icon="fa-trash fa-1x ml-2"
attrs="{'invisible': [('lot_id', '=', False), ('lot_name', '=', False)]}"
title="Clean lot"
data-hotkey="G"
/>
<button
id="btn_create_lot"
class="btn-sm float-end oe_kanban_action_button btn btn-secondary border"
type="action"
name="%(action_stock_barcodes_new_lot)d"
icon="fa-plus fa-1x"
title="Create lot"
context="{'default_product_id': product_id}"
attrs="{'invisible': [('create_lot', '=', False)]}"
help="Create new lot"
/>
</div>
</group>
<group
name="option_qty_info"
attrs="{'invisible': ['|', ('product_id', '=', False),'|', ('is_manual_qty', '=', True), ('manual_entry', '=', True)]}"
col="2"
>
<div class="m-0" colspan="2">
<strong class="d-none d-sm-block">Total Qty</strong>
<span
class="fa fa-hashtag d-sm-none oe_span_small_icon"
title="Total Quantity"
/>
<field
name="product_qty"
attrs="{'readonly': [('manual_entry', '=', False)]}"
force_save="1"
style="width:85%"
class="h5"
/>
</div>
</group>
<group
name="option_qty"
attrs="{'invisible': ['|', ('product_id', '=', False),'&amp;', ('is_manual_qty', '=', False), ('manual_entry', '=', False)]}"
col="2"
>
<div
class="row mt8"
name="option_qty_header"
colspan="2"
>
<div
class="text-center col"
name="total_qty_header"
>
<div
attrs="{'invisible': [('total_product_uom_qty', '=', 0.0)]}"
>
<span>(
<field
name="total_product_qty_done"
class="oe_inline"
readonly="1"
/>
/<field
name="total_product_uom_qty"
class="oe_inline"
readonly="1"
/>)
<field
name="product_uom_id"
class="oe_inline"
options="{'no_open': True}"
readonly="1"
widget="selection"
/>
</span>
</div>
</div>
</div>
<div
class="col text-center"
name="total_qty_field"
colspan="2"
>
<field
name="product_qty"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
</div>
</group>
</group>
</div>
<div
class="oe_stock_barcodes_bottombar d-flex"
attrs="{'invisible': ['|',('display_menu', '=', True), ('enable_add_product', '=', False)]}"
>
<field name="display_menu" invisible="1" />
<field name="enable_add_product" invisible="1" />
<field
name="manual_entry"
widget="barcode_boolean_toggle"
class="d-none"
/>
<div
class="btn-group dropup"
attrs="{'invisible': [('display_menu', '=', True)]}"
/>
<!-- HACK: To avoid inheritance crash -->
<button name="action_manual_entry" invisible="1" />
<!-- Hide button in view to allow do onclick in JS. Use d-none instead of
invisible attribute to be allocated by jquery selector
-->
<button
name="dummy_on_barcode_scanned"
id="dummy_on_barcode_scanned"
type="object"
data-hotkey="99"
invisible="0"
class="d-none"
/>
<button
name="action_add_scan_manual"
type="object"
title="ADD PRODUCT"
icon="fa-plus fa-2x"
class="btn-secondary w-100 oe_kanban_action_button btn-sm text-uppercase
d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|',('display_menu', '=', True), ('enable_add_product', '=', False)]}"
style="width: 50px"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Product</span>
</button>
<!-- // -->
<button
name="action_clean_values"
type="object"
icon="fa-trash-o fa-2x"
title="Clean Values"
class="btn-warning w-100 oe_kanban_action_button btn-sm ps-3 pe-3 mx-1
text-uppercase d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|',('display_menu', '=', True), ('manual_entry', '=', False)]}"
data-hotkey="7"
>
<span class="fs-1 d-none d-lg-block">Clean Values</span>
</button>
<button
name="action_confirm"
type="object"
icon="fa-check fa-2x"
title="Confirm"
class="btn-success w-100 oe_kanban_action_button btn-sm ps-3 pe-3 text-uppercase d-flex justify-content-center align-items-center"
attrs="{'invisible': ['|', '|','&amp;', ('is_manual_confirm', '=', False), ('manual_entry', '=', False), ('display_menu', '=', True), ('visible_force_done', '=', True)]}"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Confirm</span>
</button>
<button
name="action_force_done"
type="object"
icon="fa-check fa-2x"
title="Force done"
attrs="{'invisible': [('visible_force_done', '=', False)]}"
class="btn-danger w-100 oe_kanban_action_button btn-sm ps-3 pe-3 text-uppercase d-flex justify-content-center align-items-center"
style="width: 50px"
data-hotkey="8"
>
<span class="fs-1 d-none d-lg-block">Confirm</span>
</button>
</div>
</div>
</form>
</field>
</record>
<record id="view_stock_barcodes_read_packaging_form" model="ir.ui.view">
<field name="name">stock.barcodes.read.packaging.form</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="inherit_id" ref="stock_barcodes.view_stock_barcodes_read_form" />
<field name="arch" type="xml">
<group name="option_qty" position="before">
<group name="packaging" col="1">
<div colspan="2" groups="product.group_stock_packaging">
<field name="product_packaging_ids" invisible="1" />
<span
class="fa fa-archive d-sm-none oe_span_small_icon"
title="Source Location"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
<field
name="packaging_id"
options="{'no_open': True, 'no_create': True}"
domain="[('product_id', '=', product_id)]"
force_save="1"
style="width: 85%"
class="h5"
placeholder="Packaging"
nolabel="1"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
</div>
</group>
</group>
<xpath
expr="//div[@name='total_qty_field']//field[@name='product_qty']"
position="before"
>
<field
name="packaging_qty"
force_save="1"
widget="numeric_step"
options="{'auto_select': True}"
groups="product.group_stock_packaging"
attrs="{'invisible': [('product_packaging_ids', '=', [])]}"
/>
</xpath>
<!-- <div name="total_qty_field" position="before">
<div class="col text-center" groups="product.group_stock_packaging">
</div>
</div> -->
</field>
</record>
<!--
Open wizard in current target option to avoid that the wizard is
closed after any button click,
-->
<record id="action_stock_barcodes_read" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.read</field>
<field name="name">Barcodes Read</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="view_id" ref="view_stock_barcodes_read_form" />
<field name="target">fullscreen</field>
</record>
<record id="view_stock_barcodes_read_form_manual_qty" model="ir.ui.view">
<field name="name">stock.barcodes.read.form.manual_qty</field>
<field name="model">wiz.stock.barcodes.read</field>
<field name="priority" eval="999" />
<field name="arch" type="xml">
<form string="Barcodes manual quantities">
<sheet>
<field name="packaging_id" invisible="1" />
<field
name="packaging_qty"
attrs="{'invisible': [('packaging_id', '=', False)]}"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
<field
name="product_qty"
force_save="1"
nolabel="1"
widget="numeric_step"
options="{'auto_select': True}"
/>
<button
name="action_reopen_wizard"
type="object"
icon="fa-check"
title="Reopen"
class="btn-success"
/>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,60 @@
# 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 WizStockBarcodesNewLot(models.TransientModel):
_inherit = "barcodes.barcode_events_mixin"
_name = "wiz.stock.barcodes.new.lot"
_description = "Wizard to create new lot from barcode scanner"
product_id = fields.Many2one(comodel_name="product.product", required=True)
lot_name = fields.Char(string="Lot name")
def on_barcode_scanned(self, barcode):
product = self.env["product.product"].search([("barcode", "=", barcode)])[:1]
if product and not self.product_id:
self.product_id = product
return
self.lot_name = barcode
def _prepare_lot_values(self):
return {
"product_id": self.product_id.id,
"name": self.lot_name,
"company_id": self.env.company.id,
}
def get_scan_wizard(self):
return self.env[self.env.context["active_model"]].browse(
self.env.context["active_id"]
)
def scan_wizard_action(self):
if self.env.context.get("active_model") == "wiz.stock.barcodes.read.inventory":
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_inventory"
)
else:
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_barcodes.action_stock_barcodes_read_picking"
)
wiz_id = self.get_scan_wizard()
action["res_id"] = wiz_id.id
return action
def confirm(self):
ProductionLot = self.env["stock.lot"]
lot = ProductionLot.search(
[("product_id", "=", self.product_id.id), ("name", "=", self.lot_name)]
)
if not lot:
lot = self.env["stock.lot"].create(self._prepare_lot_values())
# Assign lot created or found to wizard scanning barcode lot_id field
wiz = self.get_scan_wizard()
if wiz:
wiz.lot_id = lot
return self.scan_wizard_action()
def cancel(self):
return self.scan_wizard_action()

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_barcodes_new_lot" model="ir.ui.view">
<field name="name">stock.barcodes.new.lot.form</field>
<field name="model">wiz.stock.barcodes.new.lot</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="alert alert-info text-center" role="alert">
<p>1 - Scan or input product barcode</p>
<p>2 - Scan or input product lot barcode</p>
</div>
<group>
<field
name="_barcode_scanned"
widget="barcode_handler"
invisible="0"
/>
<field name="product_id" />
<field name="lot_name" />
</group>
<div>
<button
string="Confirm"
name="confirm"
type="object"
class="btn-primary oe_kanban_action_button"
/>
<button
string="Cancel"
name="cancel"
class="btn-default oe_kanban_action_button"
type="object"
/>
</div>
</sheet>
</form>
</field>
</record>
<record id="action_stock_barcodes_new_lot" model="ir.actions.act_window">
<field name="res_model">wiz.stock.barcodes.new.lot</field>
<field name="name">New Lot</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="view_id" ref="view_stock_barcodes_new_lot" />
<field name="target">inline</field>
</record>
</odoo>