diff --git a/barcode_stock/README.rst b/barcode_stock/README.rst new file mode 100644 index 0000000..529e1c4 --- /dev/null +++ b/barcode_stock/README.rst @@ -0,0 +1,150 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============= +Barcode Stock +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a2042a74df318ab5cf8cd2a2d9cd1bad929621bfd1870b5ae0809b1eaf72571a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fbarcode--interface-lightgray.png?logo=github + :target: https://github.com/OCA/barcode-interface/tree/18.0/barcode_stock + :alt: OCA/barcode-interface +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/barcode-interface-18-0/barcode-interface-18-0-barcode_stock + :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/barcode-interface&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is the **warehouse application** of the Barcode suite. It +builds on the ``barcode_scanner`` base framework and registers the stock +operation screens, scan handlers and menu tiles into it. + +It covers the following operations: + +- Receipts (incoming shipments) +- Deliveries (outgoing shipments) +- Internal transfers +- Quick product and location information + +Only EAN13 barcodes are supported. Real-time validation and +audio/vibration feedback are provided on each scan. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Scan EAN13 barcodes to quickly process warehouse operations. + +Receipts +-------- + +From the main menu, select **Warehouse Operations**, then choose +**Receipts** to validate incoming goods. Scan product barcodes to +register quantities. + +Deliveries +---------- + +From **Warehouse Operations**, select **Delivery Orders** to pick and +ship items. Scan product barcodes to confirm each item. + +Internal Transfers +------------------ + +Use the **Internal Transfer** screen from the main menu to move stock +between locations. Select the origin and destination locations, then +scan or search products to add transfer lines. + +Quantities from the barcode +--------------------------- + +A scan normally counts as one unit. When the barcode states a quantity — +as a GS1 label does, with ``barcode_gs1`` installed — that quantity is +taken instead, in the product's own unit of measure: + +- a label that states a **measure** (a net weight, for instance) is the + quantity when the product is stocked in that kind of unit, converted + if needed: 2.497 kg on the label becomes 2.497 for a product in + kilograms and 2497 for one in grams; +- when the product is *not* stocked that way — a box counted in units — + the **piece count** on the label is the quantity, and a single unit + when the label states no count. A weight never turns into a number of + units. + +Quick Info +---------- + +Select **Quick Info** from the main menu, then scan any product or +location barcode to view information. Alternatively, search by barcode +or browse the product and location lists. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Binhex + +Contributors +------------ + +- `Antonio David Ruban Espinal `__ +- `Zuzanna Elzbieta Szalaty + Szalaty `__ + +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. + +.. |maintainer-szalatyzuzanna| image:: https://github.com/szalatyzuzanna.png?size=40px + :target: https://github.com/szalatyzuzanna + :alt: szalatyzuzanna + +Current `maintainer `__: + +|maintainer-szalatyzuzanna| + +This module is part of the `OCA/barcode-interface `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/barcode_stock/__init__.py b/barcode_stock/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/barcode_stock/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/barcode_stock/__manifest__.py b/barcode_stock/__manifest__.py new file mode 100644 index 0000000..56143ee --- /dev/null +++ b/barcode_stock/__manifest__.py @@ -0,0 +1,52 @@ +{ + "name": "Barcode Stock", + "version": "18.0.1.0.0", + "category": "Inventory/Logistics", + "summary": "Warehouse operations (receipts, deliveries, internal transfers) " + "for the Barcode suite", + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/barcode-interface", + "maintainers": ["szalatyzuzanna"], + "license": "AGPL-3", + "depends": [ + "barcode_scanner", + "stock", + "stock_move_line_qty_picked", + ], + "data": [ + "views/stock_location_views.xml", + ], + "assets": { + "web.assets_backend": [ + "barcode_stock/static/src/scss/picking_filters.scss", + "barcode_stock/static/src/xml/barcode_stock_templates.xml", + "barcode_stock/static/src/js/utils/move_progress.esm.js", + "barcode_stock/static/src/js/utils/avatar.esm.js", + "barcode_stock/static/src/js/services/barcode_scanner_state.esm.js", + "barcode_stock/static/src/js/services/barcode_scanner_sync.esm.js", + "barcode_stock/static/src/js/components/picking_move_card.esm.js", + "barcode_stock/static/src/js/components/picking_move_list.esm.js", + "barcode_stock/static/src/js/components/picking_done_card.esm.js", + "barcode_stock/static/src/js/components/picking_done_list.esm.js", + "barcode_stock/static/src/js/components/image_viewer.esm.js", + "barcode_stock/static/src/js/components/picking_info_tab.esm.js", + "barcode_stock/static/src/js/screens/warehouse_ops.esm.js", + "barcode_stock/static/src/js/screens/picking_list_screen.esm.js", + "barcode_stock/static/src/js/screens/picking_screen.esm.js", + "barcode_stock/static/src/js/screens/move_wizard_screen.esm.js", + "barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js", + "barcode_stock/static/src/js/screens/location_selector_screen.esm.js", + "barcode_stock/static/src/js/screens/product_selector_screen.esm.js", + "barcode_stock/static/src/js/screens/user_selector_screen.esm.js", + "barcode_stock/static/src/js/screens/quick_info.esm.js", + "barcode_stock/static/src/js/handlers/stock_scan_handlers.esm.js", + "barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js", + ], + "web.assets_unit_tests": [ + "barcode_stock/static/tests/**/*", + ], + }, + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/barcode_stock/i18n/barcode_stock.pot b/barcode_stock/i18n/barcode_stock.pot new file mode 100644 index 0000000..3ecd37d --- /dev/null +++ b/barcode_stock/i18n/barcode_stock.pot @@ -0,0 +1,1600 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * barcode_stock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-14 08:37+0000\n" +"PO-Revision-Date: 2026-09-14 08:37+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_quant.py:0 +msgid "" +"A lot/serial number is required for product %(product)s before applying the " +"inventory adjustment." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABILITY" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE LOCATIONS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE PRODUCTS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE USERS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Add a filter first, then save it as the default." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Add a line" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Add at least one product to transfer." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Add at least one product." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Add notes..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Adjust filters or scan a picking name to continue." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "All" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "All operations completed" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "All products are available." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to cancel reservations?" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to check availability?" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to delete this movement?" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Availability check error: %(error)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Availability check failed." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Availability check successful." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Available:" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Back" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Back to Dashboard" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Back to pick" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Barcode" +msgstr "" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_res_users__barcode_default_filters +msgid "Barcode Default Filters" +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_barcode_nomenclature +msgid "Barcode Nomenclature" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Barcode Scanner Internal Transfer" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Barcode not found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Barcode not recognized." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "CHECK" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Cancel Reservation error: %(error)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Cancel Reservation successfully" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Cancel reservation" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Changes saved successfully" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Completed" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Product" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Responsible" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm availability check" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm cancel reservations" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm deletion" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Confirm move" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Confirm the quantity and return to the picking queue." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Cost / Sale Price" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Could not start camera: " +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Create New Lot" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Create lot & confirm" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Create the lot first, then confirm the move in one pass." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Current Stock (" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Custom date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "DESTINATION LOCATION" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "DETAILED OPERATIONS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default filter cleared for %(operation)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default filter for %(operation)s — tap to remove." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Delete picture" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Delivery Order" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Delivery Orders" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Demand:" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Destination location" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Destination location is required." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Destination location selected: %(location)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__qty_done_total +msgid "Done" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Done lines" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Done:" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Enter a barcode." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Enter barcode" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Enter lot/serial" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Error loading picking: %(error)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Error while saving" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Filter saved as the default for %(operation)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Filters" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "From" +msgstr "" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__is_fully_picked +msgid "Fully Picked" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Group" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Group by" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "ITEM" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Information" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Internal Transfer" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Internal Transfers" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Internal transfer %(name)s validated successfully." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Internal transfer failed." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Invalid quantity" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Items" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading locations..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading move..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading products..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading queue..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading users..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s has passed its expiration date." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s is not available for this product." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Lot %(lot)s is not available here." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s matched and ready to confirm." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Lot %(lot)s not found for this product." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s ready to be registered." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot expired" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot name required" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot number" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot tracked" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Lot/Serial Number is required for product %(name)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lots can only be created in receptions" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Manual Barcode" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Move Operation" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Move stock between locations" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No confirmed scans yet" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No customer" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "No expiration date" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "" +"No internal transfer operation type is configured. Please set one up before " +"using barcode transfers." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_quant.py:0 +msgid "No inventory lines to apply." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/location_selector_screen.esm.js:0 +msgid "No location matches “%(code)s”." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No locations found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No operation found for “%(code)s”." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No pending pickings" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/product_selector_screen.esm.js:0 +msgid "No product matches “%(code)s”." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No products found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No schedule" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "" +"No stock is available in the origin location for the selected products." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No stock on hand for this lot" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No time" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "No tracking" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No users found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Notes" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "ORIGIN LOCATION" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "On Hand" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "One of the selected products no longer exists." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Open" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Operation opened from scan." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Origin and destination locations must be different." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Origin location is required." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Origin location selected: %(location)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Other" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "PARTNER" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Pending moves" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Pick Existing Lot" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Pick and ship items" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picked" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Picking" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picking Operations" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picking attachment" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Picking opened from scan." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Picking task" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Pickings" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Please add at least one product to validate." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select a destination location." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Please select a lot" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select a lot/serial number for %(product)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select an origin location." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Preparing scanner-first work queue..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Process products from the To Do tab to build the done log here." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Product" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "" +"Product %(name)s is tracked by serial number and must be transferred one " +"unit at a time." +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product matched and ready to confirm." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product not found" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Product not found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product not reserved" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product updated" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Progress" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Qty unavailable" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Quantity incremented to %(qty)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Quantity must be greater than zero for %(product)s." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Quantity must be greater than zero for product %(name)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quantity stepper" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quantity to pick" +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Queue ready" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick Info" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick Information" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick filters" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "RESPONSIBLE" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Ready" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Receipt" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Receipts" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Reception task" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Register a new lot before assigning the picked quantity." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Rem:" +msgstr "" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__qty_remaining +msgid "Remaining Quantity" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Remaining quantity applied" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Required" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Reserved:" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Responsible" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Responsible set to %(name)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "S" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "SAVE" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "SKU" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Save this filter as the default for %(operation)s." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan a product or location barcode" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Scan narrowed the queue." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan or search pickings..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan target" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scanned Lot / Serial" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Scanned barcode is not a product of this operation." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanned barcode is not part of the current picking." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "" +"Scanned product doesn't match this move (%(expected)s expected, got %(got)s)" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanned product was not found." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanner ready for this picking." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scheduled Date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search Barcode" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search by name or SKU..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search by name or login..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Search failed." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search location..." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Product" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Responsible" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Select date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select destination" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Select destination location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select lot" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select origin" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Select origin location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select responsible" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select source" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Select the correct lot or serial and confirm the quantity." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial already exists for this product." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial number" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial tracked" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial-tracked products can only process one unit." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial-tracked products must have a quantity of 1." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Set default" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Single picking matched the scan." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Source location" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Status" +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Stock by Location (" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "TASK STATUS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "TO DO" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Tap to remove this confirmation" +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_move_line.py:0 +msgid "" +"The quantity for product %(product)s (tracked by serial) cannot exceed 1." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "The selected Lot/Serial Number is invalid for product %(name)s." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "The selected locations are not valid anymore." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "There are no pending products for this picking." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "This move has no quantity to process." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "This product is not reserved for this picking." +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "To" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "To Do" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Today" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Tomorrow" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Tracking" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Tracking / Type" +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "UNITS" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Unknown" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Urgent" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Usage" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Use an existing lot or serial already available for this product." +msgstr "" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_res_users +msgid "User" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "VALIDATE" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Validate incoming goods" +msgstr "" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move_line__validated_on_date +msgid "Validated On Date" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Validation failed: %(error)s" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Visible" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Warehouse Operations" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Yes" +msgstr "" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/components/picking_move_card.esm.js:0 +msgid "pending" +msgstr "" diff --git a/barcode_stock/i18n/es.po b/barcode_stock/i18n/es.po new file mode 100644 index 0000000..60a8e9a --- /dev/null +++ b/barcode_stock/i18n/es.po @@ -0,0 +1,1619 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * barcode_stock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-14 08:37+0000\n" +"PO-Revision-Date: 2026-07-31 11:44+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_quant.py:0 +msgid "" +"A lot/serial number is required for product %(product)s before applying the " +"inventory adjustment." +msgstr "" +"Se requiere un lote/número de serie para el producto %(product)s antes de " +"aplicar el ajuste de inventario." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABILITY" +msgstr "DISPONIBILIDAD" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE LOCATIONS" +msgstr "UBICACIONES DISPONIBLES" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE PRODUCTS" +msgstr "PRODUCTOS DISPONIBLES" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "AVAILABLE USERS" +msgstr "USUARIOS DISPONIBLES" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Add a filter first, then save it as the default." +msgstr "Añade primero un filtro y luego guárdalo como predeterminado." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Add a line" +msgstr "Añadir una línea" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Add at least one product to transfer." +msgstr "Añada al menos un producto al traslado." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Add at least one product." +msgstr "Añada al menos un producto." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Add notes..." +msgstr "Añadir notas..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Adjust filters or scan a picking name to continue." +msgstr "Ajuste los filtros o escanee el nombre de un albarán para continuar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "All" +msgstr "Todos" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "All operations completed" +msgstr "Todas las operaciones completadas" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "All products are available." +msgstr "Todos los productos están disponibles." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to cancel reservations?" +msgstr "¿Seguro que quiere cancelar las reservas?" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to check availability?" +msgstr "¿Seguro que quiere comprobar la disponibilidad?" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Are you sure you want to delete this movement?" +msgstr "¿Seguro que quiere eliminar este movimiento?" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Availability check error: %(error)s" +msgstr "Error al comprobar la disponibilidad: %(error)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Availability check failed." +msgstr "Falló la comprobación de disponibilidad." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Availability check successful." +msgstr "Comprobación de disponibilidad correcta." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Available:" +msgstr "Disponible:" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Back" +msgstr "Atrás" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Back to Dashboard" +msgstr "Volver al panel" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Back to pick" +msgstr "Volver a la preparación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Barcode" +msgstr "Código de barras" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_res_users__barcode_default_filters +msgid "Barcode Default Filters" +msgstr "Filtros por defecto del código de barras" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_barcode_nomenclature +msgid "Barcode Nomenclature" +msgstr "Nomenclatura de código de barras" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Barcode Scanner Internal Transfer" +msgstr "Transferencia interna del escáner de códigos de barras" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Barcode not found." +msgstr "Código de barras no encontrado." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Barcode not recognized." +msgstr "Código de barras no reconocido." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "CHECK" +msgstr "COMPROBAR" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Cancel Reservation error: %(error)s" +msgstr "Error al cancelar la reserva: %(error)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Cancel Reservation successfully" +msgstr "Reserva cancelada correctamente" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Cancel reservation" +msgstr "Cancelar reserva" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Changes saved successfully" +msgstr "Cambios guardados correctamente" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Completed" +msgstr "Completado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Location" +msgstr "Confirmar ubicación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Product" +msgstr "Confirmar producto" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Confirm Responsible" +msgstr "Confirmar responsable" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm availability check" +msgstr "Confirmar comprobación de disponibilidad" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm cancel reservations" +msgstr "Confirmar cancelación de reservas" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Confirm deletion" +msgstr "Confirmar eliminación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Confirm move" +msgstr "Confirmar movimiento" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Confirm the quantity and return to the picking queue." +msgstr "Confirme la cantidad y vuelva a la cola de preparación." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Cost / Sale Price" +msgstr "Coste / Precio de venta" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Could not start camera: " +msgstr "No se pudo iniciar la cámara: " + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Create New Lot" +msgstr "Crear nuevo lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Create lot & confirm" +msgstr "Crear lote y confirmar" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Create the lot first, then confirm the move in one pass." +msgstr "Cree primero el lote y luego confirme el movimiento en un solo paso." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Current Stock (" +msgstr "Stock actual (" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Custom date" +msgstr "Fecha personalizada" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "DESTINATION LOCATION" +msgstr "UBICACIÓN DE DESTINO" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "DETAILED OPERATIONS" +msgstr "OPERACIONES DETALLADAS" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Date" +msgstr "Fecha" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default" +msgstr "Predeterminado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default filter cleared for %(operation)s." +msgstr "Filtro predeterminado eliminado de %(operation)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Default filter for %(operation)s — tap to remove." +msgstr "Filtro predeterminado de %(operation)s — toca para quitarlo." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Delete picture" +msgstr "Eliminar imagen" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Delivery Order" +msgstr "Albarán de entrega" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Delivery Orders" +msgstr "Albaranes de entrega" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Demand:" +msgstr "Demanda:" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Destination location" +msgstr "Ubicación de destino" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Destination location is required." +msgstr "La ubicación de destino es obligatoria." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Destination location selected: %(location)s" +msgstr "Ubicación de destino seleccionada: %(location)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__qty_done_total +msgid "Done" +msgstr "Hecho" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Done lines" +msgstr "Líneas hechas" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Done:" +msgstr "Hecho:" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Enter a barcode." +msgstr "Introduzca un código de barras." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Enter barcode" +msgstr "Introduzca el código de barras" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Enter lot/serial" +msgstr "Introduzca lote/número de serie" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Error loading picking: %(error)s" +msgstr "Error al cargar el albarán: %(error)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Error while saving" +msgstr "Error al guardar" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Filter saved as the default for %(operation)s." +msgstr "Filtro guardado como predeterminado de %(operation)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Filters" +msgstr "Filtros" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "From" +msgstr "Desde" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__is_fully_picked +msgid "Fully Picked" +msgstr "Totalmente preparado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Group" +msgstr "Grupo" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Group by" +msgstr "Agrupar por" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "ITEM" +msgstr "ARTÍCULO" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Information" +msgstr "Información" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Internal Transfer" +msgstr "Transferencia interna" + +#. module: barcode_stock +#. odoo-javascript +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Internal Transfers" +msgstr "Transferencias internas" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Internal transfer %(name)s validated successfully." +msgstr "Transferencia interna %(name)s validada correctamente." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Internal transfer failed." +msgstr "La transferencia interna falló." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Invalid quantity" +msgstr "Cantidad no válida" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Items" +msgstr "Artículos" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading locations..." +msgstr "Cargando ubicaciones..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading move..." +msgstr "Cargando movimiento..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading products..." +msgstr "Cargando productos..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading queue..." +msgstr "Cargando cola..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading users..." +msgstr "Cargando usuarios..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Loading..." +msgstr "Cargando..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Location" +msgstr "Ubicación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s has passed its expiration date." +msgstr "El lote %(lot)s ha superado su fecha de caducidad." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s is not available for this product." +msgstr "El lote %(lot)s no está disponible para este producto." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Lot %(lot)s is not available here." +msgstr "El lote %(lot)s no está disponible aquí." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s matched and ready to confirm." +msgstr "Lote %(lot)s encontrado y listo para confirmar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Lot %(lot)s not found for this product." +msgstr "No se encontró el lote %(lot)s para este producto." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Lot %(lot)s ready to be registered." +msgstr "Lote %(lot)s listo para registrar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot expired" +msgstr "Lote caducado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot name required" +msgstr "Se requiere el nombre del lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot number" +msgstr "Número de lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lot tracked" +msgstr "Seguimiento por lote" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Lot/Serial Number is required for product %(name)s." +msgstr "Se requiere lote/número de serie para el producto %(name)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Lots can only be created in receptions" +msgstr "Los lotes solo se pueden crear en las recepciones" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Manual Barcode" +msgstr "Código de barras manual" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Move Operation" +msgstr "Operación de movimiento" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Move stock between locations" +msgstr "Mover stock entre ubicaciones" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No" +msgstr "No" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No confirmed scans yet" +msgstr "Todavía no hay escaneos confirmados" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No customer" +msgstr "Sin cliente" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No date" +msgstr "Sin fecha" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "No expiration date" +msgstr "Sin fecha de caducidad" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "" +"No internal transfer operation type is configured. Please set one up before " +"using barcode transfers." +msgstr "" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_quant.py:0 +msgid "No inventory lines to apply." +msgstr "No hay líneas de inventario que aplicar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/location_selector_screen.esm.js:0 +msgid "No location matches “%(code)s”." +msgstr "Ninguna ubicación coincide con «%(code)s»." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No locations found." +msgstr "No se han encontrado ubicaciones." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No operation found for “%(code)s”." +msgstr "No se ha encontrado ninguna operación para «%(code)s»." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No pending pickings" +msgstr "No hay albaranes pendientes" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/product_selector_screen.esm.js:0 +msgid "No product matches “%(code)s”." +msgstr "Ningún producto coincide con «%(code)s»." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No products found." +msgstr "No se han encontrado productos." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No schedule" +msgstr "Sin planificación" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "No stock is available in the origin location for the selected products." +msgstr "" +"No hay stock disponible en la ubicación de origen para los productos " +"seleccionados." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No stock on hand for this lot" +msgstr "Sin stock disponible para este lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "No time" +msgstr "Sin hora" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "No tracking" +msgstr "Sin seguimiento" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "No users found." +msgstr "No se han encontrado usuarios." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Notes" +msgstr "Notas" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "ORIGIN LOCATION" +msgstr "UBICACIÓN DE ORIGEN" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "On Hand" +msgstr "Disponible" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "One of the selected products no longer exists." +msgstr "Uno de los productos seleccionados ya no existe." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Open" +msgstr "Abierto" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Operation opened from scan." +msgstr "Operación abierta desde el escaneo." + +#. module: barcode_stock +#. odoo-javascript +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Origin and destination locations must be different." +msgstr "Las ubicaciones de origen y destino deben ser diferentes." + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Origin location is required." +msgstr "La ubicación de origen es obligatoria." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Origin location selected: %(location)s" +msgstr "Ubicación de origen seleccionada: %(location)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Other" +msgstr "Otro" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "PARTNER" +msgstr "EMPRESA" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Pending moves" +msgstr "Movimientos pendientes" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Pick Existing Lot" +msgstr "Elegir lote existente" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Pick and ship items" +msgstr "Preparar y enviar artículos" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picked" +msgstr "Preparado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Picking" +msgstr "Albarán" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picking Operations" +msgstr "Operaciones de preparación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Picking attachment" +msgstr "Adjunto del albarán" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Picking opened from scan." +msgstr "Albarán abierto desde el escaneo." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Picking task" +msgstr "Tarea de preparación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Pickings" +msgstr "Albaranes" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Please add at least one product to validate." +msgstr "Añada al menos un producto para validar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select a destination location." +msgstr "Seleccione una ubicación de destino." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Please select a lot" +msgstr "Seleccione un lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select a lot/serial number for %(product)s." +msgstr "Seleccione un número de lote/serie para %(product)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Please select an origin location." +msgstr "Seleccione una ubicación de origen." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Preparing scanner-first work queue..." +msgstr "Preparando la cola de trabajo orientada al escáner..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Process products from the To Do tab to build the done log here." +msgstr "" +"Procese los productos desde la pestaña Por hacer para construir aquí el " +"registro de hechos." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Product" +msgstr "Producto" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "" +"Product %(name)s is tracked by serial number and must be transferred one " +"unit at a time." +msgstr "" +"El producto %(name)s tiene seguimiento por número de serie y debe " +"transferirse de unidad en unidad." + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimientos de producto (línea de movimiento)" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product matched and ready to confirm." +msgstr "Producto identificado y listo para confirmar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product not found" +msgstr "Producto no encontrado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Product not found." +msgstr "Producto no encontrado." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product not reserved" +msgstr "Producto no reservado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Product updated" +msgstr "Producto actualizado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Progress" +msgstr "Progreso" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Qty unavailable" +msgstr "Cantidad no disponible" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Quantity incremented to %(qty)s." +msgstr "Cantidad incrementada a %(qty)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Quantity must be greater than zero for %(product)s." +msgstr "La cantidad debe ser mayor que cero para %(product)s." + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "Quantity must be greater than zero for product %(name)s." +msgstr "La cantidad debe ser mayor que cero para el producto %(name)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quantity stepper" +msgstr "Selector de cantidad" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quantity to pick" +msgstr "Cantidad a preparar" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_quant +msgid "Quants" +msgstr "Cuantías" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Queue ready" +msgstr "Cola lista" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick Info" +msgstr "Información rápida" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick Information" +msgstr "Información rápida" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Quick filters" +msgstr "Filtros rápidos" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "RESPONSIBLE" +msgstr "RESPONSABLE" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Ready" +msgstr "Listo" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Receipt" +msgstr "Recepción" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Receipts" +msgstr "Recepciones" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Reception task" +msgstr "Tarea de recepción" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Register a new lot before assigning the picked quantity." +msgstr "Registre un nuevo lote antes de asignar la cantidad preparada." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Rem:" +msgstr "Rest.:" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move__qty_remaining +msgid "Remaining Quantity" +msgstr "Cantidad restante" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Remaining quantity applied" +msgstr "Cantidad restante aplicada" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Required" +msgstr "Necesario" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Reserved:" +msgstr "Reservado:" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Responsible" +msgstr "Responsable" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Responsible set to %(name)s" +msgstr "Responsable establecido a %(name)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "S" +msgstr "S" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "SAVE" +msgstr "GUARDAR" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "SKU" +msgstr "SKU" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Save this filter as the default for %(operation)s." +msgstr "Guardar este filtro como predeterminado de %(operation)s." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan a product or location barcode" +msgstr "Escanee el código de barras de un producto o ubicación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Scan narrowed the queue." +msgstr "El escaneo ha reducido la cola." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan or search pickings..." +msgstr "Escanee o busque albaranes..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scan target" +msgstr "Objetivo de escaneo" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scanned Lot / Serial" +msgstr "Lote / N.º de serie escaneado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Scanned barcode is not a product of this operation." +msgstr "El código de barras escaneado no es un producto de esta operación." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanned barcode is not part of the current picking." +msgstr "El código de barras escaneado no forma parte del albarán actual." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "" +"Scanned product doesn't match this move (%(expected)s expected, got %(got)s)" +msgstr "" +"El producto escaneado no coincide con este movimiento (se esperaba " +"%(expected)s, se obtuvo %(got)s)" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanned product was not found." +msgstr "No se ha encontrado el producto escaneado." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Scanner ready for this picking." +msgstr "Escáner listo para este albarán." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Scheduled Date" +msgstr "Fecha prevista" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search Barcode" +msgstr "Buscar código de barras" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search by name or SKU..." +msgstr "Buscar por nombre o SKU..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search by name or login..." +msgstr "Buscar por nombre o usuario..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/quick_info.esm.js:0 +msgid "Search failed." +msgstr "La búsqueda falló." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Search location..." +msgstr "Buscar ubicación..." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Location" +msgstr "Seleccionar ubicación" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Product" +msgstr "Seleccionar producto" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select Responsible" +msgstr "Seleccionar responsable" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Select date" +msgstr "Seleccionar fecha" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select destination" +msgstr "Seleccionar destino" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Select destination location" +msgstr "Seleccionar ubicación de destino" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select lot" +msgstr "Seleccionar lote" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select origin" +msgstr "Seleccionar origen" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js:0 +msgid "Select origin location" +msgstr "Seleccionar ubicación de origen" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select responsible" +msgstr "Seleccionar responsable" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Select source" +msgstr "Seleccionar origen" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Select the correct lot or serial and confirm the quantity." +msgstr "Seleccione el lote o número de serie correcto y confirme la cantidad." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial already exists for this product." +msgstr "El número de serie ya existe para este producto." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial number" +msgstr "Número de serie" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial tracked" +msgstr "Seguimiento por número de serie" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial-tracked products can only process one unit." +msgstr "" +"Los productos con seguimiento por número de serie solo pueden procesar una " +"unidad." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Serial-tracked products must have a quantity of 1." +msgstr "" +"Los productos con seguimiento por número de serie deben tener una cantidad " +"de 1." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Set default" +msgstr "Fijar predeterminado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Single picking matched the scan." +msgstr "Un único albarán coincide con el escaneo." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Source location" +msgstr "Ubicación de origen" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Status" +msgstr "Estado" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de stock" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Stock by Location (" +msgstr "Stock por ubicación (" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "TASK STATUS" +msgstr "ESTADO DE LA TAREA" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "TO DO" +msgstr "POR HACER" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Tap to remove this confirmation" +msgstr "Toque para eliminar esta confirmación" + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_move_line.py:0 +msgid "" +"The quantity for product %(product)s (tracked by serial) cannot exceed 1." +msgstr "" +"La cantidad para el producto %(product)s (con seguimiento por serie) no " +"puede superar 1." + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "The selected Lot/Serial Number is invalid for product %(name)s." +msgstr "" +"El lote/número de serie seleccionado no es válido para el producto %(name)s." + +#. module: barcode_stock +#. odoo-python +#: code:addons/barcode_stock/models/stock_picking.py:0 +msgid "The selected locations are not valid anymore." +msgstr "Las ubicaciones seleccionadas ya no son válidas." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "There are no pending products for this picking." +msgstr "No hay productos pendientes para este albarán." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "This move has no quantity to process." +msgstr "Este movimiento no tiene cantidad que procesar." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "This product is not reserved for this picking." +msgstr "Este producto no está reservado para este albarán." + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "To" +msgstr "Hasta" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "To Do" +msgstr "Por hacer" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Today" +msgstr "Hoy" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Tomorrow" +msgstr "Mañana" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Tracking" +msgstr "Seguimiento" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Tracking / Type" +msgstr "Seguimiento / Tipo" + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_stock_picking +msgid "Transfer" +msgstr "Traslado" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "UNITS" +msgstr "UNIDADES" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Unknown" +msgstr "Desconocido" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Urgent" +msgstr "Urgente" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Usage" +msgstr "Uso" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js:0 +msgid "Use an existing lot or serial already available for this product." +msgstr "" +"Utilice un lote o número de serie existente ya disponible para este producto." + +#. module: barcode_stock +#: model:ir.model,name:barcode_stock.model_res_users +msgid "User" +msgstr "Usuario" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "VALIDATE" +msgstr "VALIDAR" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Validate incoming goods" +msgstr "Validar la mercancía entrante" + +#. module: barcode_stock +#: model:ir.model.fields,field_description:barcode_stock.field_stock_move_line__validated_on_date +msgid "Validated On Date" +msgstr "Validado en fecha" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_screen.esm.js:0 +msgid "Validation failed: %(error)s" +msgstr "La validación falló: %(error)s" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Visible" +msgstr "Visible" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/tiles/stock_menu_tiles.esm.js:0 +#: code:addons/barcode_stock/static/src/xml/barcode_stock_templates.xml:0 +msgid "Warehouse Operations" +msgstr "Operaciones de almacén" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/screens/picking_list_screen.esm.js:0 +msgid "Yes" +msgstr "Sí" + +#. module: barcode_stock +#. odoo-javascript +#: code:addons/barcode_stock/static/src/js/components/picking_move_card.esm.js:0 +msgid "pending" +msgstr "pendiente" + diff --git a/barcode_stock/models/__init__.py b/barcode_stock/models/__init__.py new file mode 100644 index 0000000..3bc9734 --- /dev/null +++ b/barcode_stock/models/__init__.py @@ -0,0 +1,6 @@ +from . import stock_picking +from . import stock_move +from . import stock_quant +from . import barcode_nomenclature +from . import stock_move_line +from . import res_users diff --git a/barcode_stock/models/barcode_nomenclature.py b/barcode_stock/models/barcode_nomenclature.py new file mode 100644 index 0000000..2494791 --- /dev/null +++ b/barcode_stock/models/barcode_nomenclature.py @@ -0,0 +1,18 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class BarcodeNomenclature(models.Model): + _inherit = "barcode.nomenclature" + + @api.model + def parse_barcode_scanner_barcode(self, barcode): + nomenclature = self.env.ref("barcodes.default_barcode_nomenclature") + parsed = nomenclature.parse_barcode(barcode) + return { + "type": "ean", + "product": parsed.get("code"), + "qty": parsed.get("value", 1), + } diff --git a/barcode_stock/models/res_users.py b/barcode_stock/models/res_users.py new file mode 100644 index 0000000..e2cb544 --- /dev/null +++ b/barcode_stock/models/res_users.py @@ -0,0 +1,25 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + # Per-operation-type default filters for the Barcode picking list, kept as a + # JSON map keyed by operation-type code, e.g. + # {"incoming": {"activeFilters": ["date"], "filterValues": {"date": "today"}}} + # The frontend owns the shape; the server only stores and serves the map so + # the app can apply a user's starred filter automatically on entry. + barcode_default_filters = fields.Json( + default=dict, + ) + + @property + def SELF_READABLE_FIELDS(self): + return super().SELF_READABLE_FIELDS + ["barcode_default_filters"] + + @property + def SELF_WRITEABLE_FIELDS(self): + return super().SELF_WRITEABLE_FIELDS + ["barcode_default_filters"] diff --git a/barcode_stock/models/stock_move.py b/barcode_stock/models/stock_move.py new file mode 100644 index 0000000..38906ad --- /dev/null +++ b/barcode_stock/models/stock_move.py @@ -0,0 +1,73 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + qty_done_total = fields.Float( + compute="_compute_qty_progress", + string="Done", + store=False, + ) + qty_remaining = fields.Float( + compute="_compute_qty_progress", + string="Remaining Quantity", + store=False, + ) + is_fully_picked = fields.Boolean( + compute="_compute_qty_progress", + string="Fully Picked", + store=False, + ) + + def _reset_qty_progress(self): + for move in self: + move.qty_done_total = 0 + move.qty_remaining = 0 + move.is_fully_picked = False + + @api.depends("move_line_ids.qty_picked", "picking_id.state") + def _compute_qty_progress(self): + if self.env.context.get("install_mode"): + self._reset_qty_progress() + return + ready_moves = self.filtered(lambda m: m.picking_id.state == "assigned") + if not ready_moves: + self._reset_qty_progress() + return + data = self.env["stock.move.line"]._read_group( + [("move_id", "in", ready_moves.ids)], + ["move_id"], + ["qty_picked:sum"], + ) + sums = {move.id: qty_picked for move, qty_picked in data} + for move in self: + done = sums.get(move.id, 0.0) + move.qty_done_total = done + move.qty_remaining = move.product_uom_qty - done + move.is_fully_picked = move.qty_remaining <= 0 + + def _upsert_move_line(self, vals): + self.ensure_one() + StockMoveLine = self.env["stock.move.line"] + domain = [("move_id", "=", self.id)] + identifying_fields = [ + "product_id", + "lot_id", + "package_id", + "result_package_id", + "location_id", + "location_dest_id", + ] + for field in identifying_fields: + if field in vals: + domain.append((field, "=", vals[field])) + move_line = StockMoveLine.search(domain, limit=1, order="id desc") + if move_line: + move_line.write(vals) + return move_line + vals.setdefault("move_id", self.id) + return StockMoveLine.create(vals) diff --git a/barcode_stock/models/stock_move_line.py b/barcode_stock/models/stock_move_line.py new file mode 100644 index 0000000..7cffce5 --- /dev/null +++ b/barcode_stock/models/stock_move_line.py @@ -0,0 +1,23 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + validated_on_date = fields.Datetime() + + @api.constrains("quantity", "product_id", "lot_id") + def _check_serial_quantity(self): + for line in self: + if line.product_id.tracking == "serial" and line.quantity > 1: + raise ValidationError( + _( + "The quantity for product %(product)s (tracked by serial) " + "cannot exceed 1.", + product=line.product_id.display_name, + ) + ) diff --git a/barcode_stock/models/stock_picking.py b/barcode_stock/models/stock_picking.py new file mode 100644 index 0000000..c8b0301 --- /dev/null +++ b/barcode_stock/models/stock_picking.py @@ -0,0 +1,368 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + @api.model + def _barcode_scanner_get_internal_picking_type( + self, origin_location, destination_location + ): + picking_type_model = self.env["stock.picking.type"] + warehouse = origin_location.warehouse_id or destination_location.warehouse_id + # The warehouse's own internal operation type is the canonical target, + # returned even when archived (a single-step warehouse keeps it inactive). + if warehouse and warehouse.int_type_id: + return warehouse.int_type_id + # Fall back to any internal type for this company (or a company-shared + # one), archived types included so a valid setup is never overlooked. + picking_type = picking_type_model.with_context(active_test=False).search( + [ + ("code", "=", "internal"), + ("company_id", "in", [self.env.company.id, False]), + ], + order="warehouse_id, id", + limit=1, + ) + if not picking_type: + # A warehouse operation is not the place to create configuration: + # the internal operation type must be set up beforehand. + raise UserError( + _( + "No internal transfer operation type is configured. " + "Please set one up before using barcode transfers." + ) + ) + return picking_type + + @api.model + def _barcode_scanner_prepare_internal_transfer( + self, + origin_location_id, + destination_location_id, + responsible_id, + lines, + ): + if not origin_location_id: + raise UserError(_("Origin location is required.")) + if not destination_location_id: + raise UserError(_("Destination location is required.")) + if not lines: + raise UserError(_("Add at least one product to transfer.")) + + origin_location = self.env["stock.location"].browse(origin_location_id).exists() + destination_location = ( + self.env["stock.location"].browse(destination_location_id).exists() + ) + if not origin_location or not destination_location: + raise UserError(_("The selected locations are not valid anymore.")) + if origin_location == destination_location: + raise UserError(_("Origin and destination locations must be different.")) + + responsible = self.env["res.users"] + if responsible_id: + responsible = self.env["res.users"].browse(responsible_id).exists() + prepared_lines = [] + for line in lines: + product = ( + self.env["product.product"].browse(line.get("product_id")).exists() + ) + if not product: + raise UserError(_("One of the selected products no longer exists.")) + qty = float(line.get("qty") or 0) + if qty <= 0: + raise UserError( + _( + "Quantity must be greater than zero for product %(name)s.", + name=product.display_name, + ) + ) + + lot = self.env["stock.lot"] + lot_id = line.get("lot_id") + if product.tracking != "none": + if not lot_id: + raise UserError( + _( + "Lot/Serial Number is required for product %(name)s.", + name=product.display_name, + ) + ) + lot = self.env["stock.lot"].browse(int(lot_id)).exists() + if not lot or lot.product_id != product: + raise UserError( + _( + "The selected Lot/Serial Number is invalid for product " + "%(name)s.", + name=product.display_name, + ) + ) + if product.tracking == "serial" and qty != 1: + raise UserError( + _( + "Product %(name)s is tracked by serial number and must be " + "transferred one unit at a time.", + name=product.display_name, + ) + ) + + prepared_lines.append( + { + "product": product, + "qty": qty, + "lot": lot, + } + ) + + return origin_location, destination_location, responsible, prepared_lines + + @api.model + def _barcode_scanner_collect_internal_transfer_availability( + self, origin_location, prepared_lines + ): + quant_model = self.env["stock.quant"] + availability_lines = [] + all_available = True + for line in prepared_lines: + product = line["product"] + lot = line["lot"] + available_qty = quant_model._get_available_quantity( + product, + origin_location, + lot_id=lot, + strict=False, + ) + is_available = ( + float_compare( + available_qty, + line["qty"], + precision_rounding=product.uom_id.rounding, + ) + >= 0 + ) + all_available &= is_available + availability_lines.append( + { + "product_id": product.id, + "product_name": product.display_name, + "lot_id": lot.id if lot else False, + "lot_name": lot.name if lot else False, + "required_qty": line["qty"], + "available_qty": available_qty, + "available": is_available, + } + ) + return { + "available": all_available, + "lines": availability_lines, + } + + @api.model + def action_barcode_scanner_check_availability( + self, + origin_location_id, + destination_location_id, + lines, + responsible_id=False, + ): + ( + origin_location, + _destination_location, + _responsible, + prepared_lines, + ) = self._barcode_scanner_prepare_internal_transfer( + origin_location_id, + destination_location_id, + responsible_id, + lines, + ) + return self._barcode_scanner_collect_internal_transfer_availability( + origin_location, + prepared_lines, + ) + + @api.model + def _barcode_scanner_allocate_from_origin( + self, origin_location, product, lot, requested_qty + ): + """Allocate up to ``requested_qty`` of ``product`` (optionally ``lot``) + against the real quants available in ``origin_location`` and its child + locations, honouring the removal strategy. + + Returns a list of ``{"location", "lot", "qty"}`` describing where the + stock is actually taken from. The candidate quants are row-locked to + avoid concurrent over-allocation. + """ + quant_model = self.env["stock.quant"] + rounding = product.uom_id.rounding + quants = quant_model._gather( + product, origin_location, lot_id=lot or None, strict=False + ) + if quants: + self.env.cr.execute( + "SELECT id FROM stock_quant WHERE id IN %s " + "FOR NO KEY UPDATE SKIP LOCKED", + [tuple(quants.ids)], + ) + locked_ids = {row[0] for row in self.env.cr.fetchall()} + quants = quants.filtered(lambda quant: quant.id in locked_ids) + + allocations = [] + remaining = requested_qty + for quant in quants: + if float_compare(remaining, 0, precision_rounding=rounding) <= 0: + break + free = quant.quantity - quant.reserved_quantity + if float_compare(free, 0, precision_rounding=rounding) <= 0: + continue + take = min(remaining, free) + allocations.append( + { + "location": quant.location_id, + "lot": quant.lot_id, + "qty": take, + } + ) + remaining -= take + return allocations + + @api.model + def action_barcode_scanner_internal_transfer( + self, + origin_location_id, + destination_location_id, + responsible_id, + lines, + ): + ( + origin_location, + destination_location, + responsible, + prepared_lines, + ) = self._barcode_scanner_prepare_internal_transfer( + origin_location_id, + destination_location_id, + responsible_id, + lines, + ) + + # Build the move plan by allocating each line against the stock actually + # on hand in the origin location *and its children*, capped at what is + # available. Like the back office, we transfer the available quantity + # instead of refusing the whole operation when more was requested. + move_plan = [] + for line in prepared_lines: + product = line["product"] + rounding = product.uom_id.rounding + allocations = self._barcode_scanner_allocate_from_origin( + origin_location, product, line["lot"], line["qty"] + ) + allocated = sum(alloc["qty"] for alloc in allocations) + if float_compare(allocated, 0, precision_rounding=rounding) <= 0: + continue + move_plan.append( + {"line": line, "allocations": allocations, "qty": allocated} + ) + + if not move_plan: + raise UserError( + _( + "No stock is available in the origin location for the " + "selected products." + ) + ) + + picking_type = self._barcode_scanner_get_internal_picking_type( + origin_location, + destination_location, + ) + picking = self.create( + { + "picking_type_id": picking_type.id, + "location_id": origin_location.id, + "location_dest_id": destination_location.id, + "move_type": "direct", + "user_id": responsible.id if responsible else False, + "origin": _("Barcode Scanner Internal Transfer"), + } + ) + + move_line_model = self.env["stock.move.line"] + move_line_vals_list = [] + for plan in move_plan: + line = plan["line"] + product = line["product"] + move = self.env["stock.move"].create( + { + "name": product.display_name, + "product_id": product.id, + "product_uom_qty": plan["qty"], + "product_uom": product.uom_id.id, + "picking_id": picking.id, + "location_id": origin_location.id, + "location_dest_id": destination_location.id, + "company_id": picking.company_id.id, + } + ) + for alloc in plan["allocations"]: + vals = { + "move_id": move.id, + "picking_id": picking.id, + "product_id": product.id, + "product_uom_id": product.uom_id.id, + "quantity": alloc["qty"], + "picked": True, + "location_id": alloc["location"].id, + "location_dest_id": destination_location.id, + "company_id": picking.company_id.id, + "lot_id": alloc["lot"].id if alloc["lot"] else False, + "lot_name": alloc["lot"].name if alloc["lot"] else False, + "qty_picked": alloc["qty"], + } + move_line_vals_list.append(vals) + + picking.action_confirm() + # Our manually-allocated move lines are authoritative; drop anything the + # confirmation may have auto-reserved so we do not double-count stock. + picking.do_unreserve() + move_line_model.create(move_line_vals_list) + picking.with_context(skip_backorder=True).button_validate() + + # Report requested vs. moved per line so the UI can warn the operator + # when only part of the requested quantity was available (allocation is + # capped at on-hand stock, and SKIP LOCKED may skip busy quants). + moved_by_line = {id(plan["line"]): plan["qty"] for plan in move_plan} + line_summary = [ + { + "product_id": line["product"].id, + "product_name": line["product"].display_name, + "requested_qty": line["qty"], + "moved_qty": moved_by_line.get(id(line), 0.0), + } + for line in prepared_lines + ] + fully_transferred = all( + float_compare( + summary["moved_qty"], + summary["requested_qty"], + precision_rounding=( + self.env["product.product"] + .browse(summary["product_id"]) + .uom_id.rounding + ), + ) + >= 0 + for summary in line_summary + ) + return { + "picking_id": picking.id, + "picking_name": picking.name, + "state": picking.state, + "fully_transferred": fully_transferred, + "lines": line_summary, + } diff --git a/barcode_stock/models/stock_quant.py b/barcode_stock/models/stock_quant.py new file mode 100644 index 0000000..551af62 --- /dev/null +++ b/barcode_stock/models/stock_quant.py @@ -0,0 +1,28 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + def action_apply_inventory(self): + for quant in self: + if quant.product_id.tracking in ("lot", "serial") and not quant.lot_id: + raise UserError( + _( + "A lot/serial number is required for product %(product)s " + "before applying the inventory adjustment.", + product=quant.product_id.display_name, + ) + ) + return super().action_apply_inventory() + + def action_apply_inventory_from_scanner(self): + if not self: + raise UserError(_("No inventory lines to apply.")) + # Go through action_apply_inventory so the scanner path enforces the same + # guards as the back office (lot requirement plus core's checks). + return self.action_apply_inventory() diff --git a/barcode_stock/pyproject.toml b/barcode_stock/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/barcode_stock/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/barcode_stock/readme/CONTRIBUTORS.md b/barcode_stock/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..7a07748 --- /dev/null +++ b/barcode_stock/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +* [Antonio David Ruban Espinal](https://github.com/antoniodavid) +* [Zuzanna Elzbieta Szalaty Szalaty](https://github.com/szalatyzuzanna) diff --git a/barcode_stock/readme/DESCRIPTION.md b/barcode_stock/readme/DESCRIPTION.md new file mode 100644 index 0000000..22f0ec6 --- /dev/null +++ b/barcode_stock/readme/DESCRIPTION.md @@ -0,0 +1,13 @@ +This module is the **warehouse application** of the Barcode suite. It builds on +the `barcode_scanner` base framework and registers the stock operation screens, +scan handlers and menu tiles into it. + +It covers the following operations: + +- Receipts (incoming shipments) +- Deliveries (outgoing shipments) +- Internal transfers +- Quick product and location information + +Only EAN13 barcodes are supported. Real-time validation and audio/vibration +feedback are provided on each scan. diff --git a/barcode_stock/readme/USAGE.md b/barcode_stock/readme/USAGE.md new file mode 100644 index 0000000..a080bc0 --- /dev/null +++ b/barcode_stock/readme/USAGE.md @@ -0,0 +1,38 @@ +Scan EAN13 barcodes to quickly process warehouse operations. + +## Receipts + +From the main menu, select **Warehouse Operations**, then choose +**Receipts** to validate incoming goods. Scan product barcodes to +register quantities. + +## Deliveries + +From **Warehouse Operations**, select **Delivery Orders** to pick and +ship items. Scan product barcodes to confirm each item. + +## Internal Transfers + +Use the **Internal Transfer** screen from the main menu to move stock +between locations. Select the origin and destination locations, then +scan or search products to add transfer lines. + +## Quantities from the barcode + +A scan normally counts as one unit. When the barcode states a quantity — as a +GS1 label does, with ``barcode_gs1`` installed — that quantity is taken instead, +in the product's own unit of measure: + +- a label that states a **measure** (a net weight, for instance) is the quantity + when the product is stocked in that kind of unit, converted if needed: 2.497 kg + on the label becomes 2.497 for a product in kilograms and 2497 for one in + grams; +- when the product is *not* stocked that way — a box counted in units — the + **piece count** on the label is the quantity, and a single unit when the label + states no count. A weight never turns into a number of units. + +## Quick Info + +Select **Quick Info** from the main menu, then scan any product or +location barcode to view information. Alternatively, search by barcode +or browse the product and location lists. diff --git a/barcode_stock/static/description/icon.png b/barcode_stock/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/barcode_stock/static/description/icon.png differ diff --git a/barcode_stock/static/description/index.html b/barcode_stock/static/description/index.html new file mode 100644 index 0000000..b1085fd --- /dev/null +++ b/barcode_stock/static/description/index.html @@ -0,0 +1,495 @@ + + + + + +Barcode Stock + + + +
+ + + +Odoo Community Association + +
+

Barcode Stock

+ +

Beta License: AGPL-3 OCA/barcode-interface Translate me on Weblate Try me on Runboat

+

This module is the warehouse application of the Barcode suite. It +builds on the barcode_scanner base framework and registers the stock +operation screens, scan handlers and menu tiles into it.

+

It covers the following operations:

+
    +
  • Receipts (incoming shipments)
  • +
  • Deliveries (outgoing shipments)
  • +
  • Internal transfers
  • +
  • Quick product and location information
  • +
+

Only EAN13 barcodes are supported. Real-time validation and +audio/vibration feedback are provided on each scan.

+

Table of contents

+ +
+

Usage

+

Scan EAN13 barcodes to quickly process warehouse operations.

+
+

Receipts

+

From the main menu, select Warehouse Operations, then choose +Receipts to validate incoming goods. Scan product barcodes to +register quantities.

+
+
+

Deliveries

+

From Warehouse Operations, select Delivery Orders to pick and +ship items. Scan product barcodes to confirm each item.

+
+
+

Internal Transfers

+

Use the Internal Transfer screen from the main menu to move stock +between locations. Select the origin and destination locations, then +scan or search products to add transfer lines.

+
+
+

Quantities from the barcode

+

A scan normally counts as one unit. When the barcode states a quantity — +as a GS1 label does, with barcode_gs1 installed — that quantity is +taken instead, in the product’s own unit of measure:

+
    +
  • a label that states a measure (a net weight, for instance) is the +quantity when the product is stocked in that kind of unit, converted +if needed: 2.497 kg on the label becomes 2.497 for a product in +kilograms and 2497 for one in grams;
  • +
  • when the product is not stocked that way — a box counted in units — +the piece count on the label is the quantity, and a single unit +when the label states no count. A weight never turns into a number of +units.
  • +
+
+
+

Quick Info

+

Select Quick Info from the main menu, then scan any product or +location barcode to view information. Alternatively, search by barcode +or browse the product and location lists.

+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Binhex
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

szalatyzuzanna

+

This module is part of the OCA/barcode-interface project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/barcode_stock/static/src/js/components/image_viewer.esm.js b/barcode_stock/static/src/js/components/image_viewer.esm.js new file mode 100644 index 0000000..21a5d29 --- /dev/null +++ b/barcode_stock/static/src/js/components/image_viewer.esm.js @@ -0,0 +1,66 @@ +import {Component, onWillUpdateProps, useState} from "@odoo/owl"; + +export class ImageViewer extends Component { + setup() { + this.state = useState({ + currentIndex: this.props.initialIndex || 0, + }); + this.touchStartX = 0; + + onWillUpdateProps((nextProps) => { + if (this.state.currentIndex >= nextProps.images.length) { + this.state.currentIndex = Math.max(0, nextProps.images.length - 1); + } + }); + } + + get currentImageId() { + return this.props.images[this.state.currentIndex]; + } + + get currentImageSrc() { + return this.currentImageId ? `/web/image/${this.currentImageId}` : ""; + } + + nextImage() { + const total = this.props.images.length; + this.state.currentIndex = (this.state.currentIndex + 1) % total; + } + + prevImage() { + const total = this.props.images.length; + this.state.currentIndex = (this.state.currentIndex - 1 + total) % total; + } + + deleteImage() { + const imgId = this.currentImageId; + if (imgId !== undefined) { + this.props.onDeleteImage(imgId); + } + } + + onTouchStart(ev) { + this.touchStartX = ev.changedTouches[0].screenX; + } + + onTouchEnd(ev) { + const touchEndX = ev.changedTouches[0].screenX; + const diff = touchEndX - this.touchStartX; + if (Math.abs(diff) < 50) { + return; + } + if (diff < 0) { + this.nextImage(); + } else { + this.prevImage(); + } + } +} + +ImageViewer.template = "barcode_scanner.ImageViewer"; +ImageViewer.props = { + images: {type: Array, element: Number}, + initialIndex: {type: Number, optional: true}, + onClose: Function, + onDeleteImage: Function, +}; diff --git a/barcode_stock/static/src/js/components/picking_done_card.esm.js b/barcode_stock/static/src/js/components/picking_done_card.esm.js new file mode 100644 index 0000000..af9390c --- /dev/null +++ b/barcode_stock/static/src/js/components/picking_done_card.esm.js @@ -0,0 +1,10 @@ +import {Component} from "@odoo/owl"; + +export class PickingDoneCard extends Component {} + +PickingDoneCard.template = "barcode_scanner.PickingDoneCard"; +PickingDoneCard.props = { + line: Object, + highlighted: {type: Boolean, optional: true}, + onDelete: Function, +}; diff --git a/barcode_stock/static/src/js/components/picking_done_list.esm.js b/barcode_stock/static/src/js/components/picking_done_list.esm.js new file mode 100644 index 0000000..0d5770c --- /dev/null +++ b/barcode_stock/static/src/js/components/picking_done_list.esm.js @@ -0,0 +1,16 @@ +import {Component} from "@odoo/owl"; +import {PickingDoneCard} from "./picking_done_card.esm"; + +export class PickingDoneList extends Component { + get doneLines() { + return this.props.lines.filter((line) => line.qty_picked > 0); + } +} + +PickingDoneList.template = "barcode_scanner.PickingDoneList"; +PickingDoneList.components = {PickingDoneCard}; +PickingDoneList.props = { + lines: {type: Array}, + highlightedMoveId: {type: [Number, "null"], optional: true}, + onDeleteLine: Function, +}; diff --git a/barcode_stock/static/src/js/components/picking_info_tab.esm.js b/barcode_stock/static/src/js/components/picking_info_tab.esm.js new file mode 100644 index 0000000..ac287ac --- /dev/null +++ b/barcode_stock/static/src/js/components/picking_info_tab.esm.js @@ -0,0 +1,33 @@ +import {Component, markup} from "@odoo/owl"; + +export class PickingInfoTab extends Component { + get noteMarkup() { + const note = this.props.picking?.note; + return note ? markup(note) : false; + } + + get notePlainText() { + const note = this.props.picking?.note; + if (!note) return ""; + const tmp = document.createElement("div"); + tmp.innerHTML = note; + return tmp.textContent || tmp.innerText || ""; + } + + onNoteInput(ev) { + const text = ev.target.value.trim(); + this.props.picking.note = text ? `

${text}

` : ""; + } + + onSelectResponsible() { + this.props.onSelectResponsible?.(); + } +} + +PickingInfoTab.template = "barcode_scanner.PickingInfoTab"; +PickingInfoTab.props = { + picking: Object, + pickingTypeCode: {type: String, optional: true}, + progressLabel: {type: String, optional: true}, + onSelectResponsible: {type: Function, optional: true}, +}; diff --git a/barcode_stock/static/src/js/components/picking_move_card.esm.js b/barcode_stock/static/src/js/components/picking_move_card.esm.js new file mode 100644 index 0000000..3b28cee --- /dev/null +++ b/barcode_stock/static/src/js/components/picking_move_card.esm.js @@ -0,0 +1,51 @@ +import {Component} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; + +export class PickingMoveCard extends Component { + get linesForMove() { + if (!this.props.moveLines) { + return []; + } + // Only list lines that already carry a lot/serial and still have + // something pending. Reserved lines without a lot showed up as noisy, + // repeated "No lot" rows -- one per unit on a serial move -- while the + // demand/done/remaining figures already cover the untracked case. + return this.props.moveLines.filter( + (line) => + line.move_id?.[0] === this.props.move.id && + line.lot_id && + line.quantity > 0 && + line.qty_picked < line.quantity + ); + } + + get remainingQty() { + return this.props.stats?.remainingQty ?? 0; + } + + get doneQty() { + return this.props.stats?.doneQty ?? this.props.move.qty_done_total ?? 0; + } + + get demandQty() { + return ( + this.props.stats?.demandQty ?? + this.props.move.quantity ?? + this.props.move.product_uom_qty ?? + 0 + ); + } + + get pendingLabel() { + return _t("pending"); + } +} + +PickingMoveCard.template = "barcode_scanner.PickingMoveCard"; +PickingMoveCard.props = { + move: Object, + moveLines: {type: Array, optional: true}, + stats: {type: Object, optional: true}, + highlighted: {type: Boolean, optional: true}, + onClick: Function, +}; diff --git a/barcode_stock/static/src/js/components/picking_move_list.esm.js b/barcode_stock/static/src/js/components/picking_move_list.esm.js new file mode 100644 index 0000000..94873a2 --- /dev/null +++ b/barcode_stock/static/src/js/components/picking_move_list.esm.js @@ -0,0 +1,33 @@ +import {Component} from "@odoo/owl"; +import {PickingMoveCard} from "./picking_move_card.esm"; +import {isMovePending} from "@barcode_stock/js/utils/move_progress.esm"; + +export class PickingMoveList extends Component { + showMove(move) { + return isMovePending(move); + } + + getMoveStats(move) { + return ( + this.props.moveStatsById?.[move.id] || { + demandQty: move.product_uom_qty || move.quantity || 0, + doneQty: move.qty_done_total || 0, + remainingQty: Math.max( + (move.quantity || 0) - (move.qty_done_total || 0), + 0 + ), + completionLabel: "0%", + } + ); + } +} + +PickingMoveList.template = "barcode_scanner.PickingMoveList"; +PickingMoveList.components = {PickingMoveCard}; +PickingMoveList.props = { + moves: {type: Array}, + moveLines: {type: Array, optional: true}, + moveStatsById: {type: Object, optional: true}, + highlightedMoveId: {type: [Number, "null"], optional: true}, + onOpenWizard: Function, +}; diff --git a/barcode_stock/static/src/js/handlers/stock_scan_handlers.esm.js b/barcode_stock/static/src/js/handlers/stock_scan_handlers.esm.js new file mode 100644 index 0000000..901cc44 --- /dev/null +++ b/barcode_stock/static/src/js/handlers/stock_scan_handlers.esm.js @@ -0,0 +1,74 @@ +import {barcodeMatchDomain} from "@barcode_scanner/js/utils/scan_match.esm"; +import {barcodeScanHandlers} from "@barcode_scanner/js/registries.esm"; + +/** + * Home-screen scan recognition for stock. Isolated in its own file so it can be + * moved to `barcode_stock` untouched in phase 2. + */ + +barcodeScanHandlers.add( + "stock_picking", + { + async handle(barcode, parsed, {api, navigate}) { + // A picking reference is client-configurable: its name comes from the + // operation type's sequence, whose prefix is the warehouse's own code + // (AC-HAU, DI-LIC, JOBS...), not necessarily WH or INT. Never gate the + // lookup on a hard-coded prefix — search by name and fall through when + // nothing matches, exactly like the product and location handlers. + const pickings = await api.searchRead( + "stock.picking", + [["name", "=", barcode]], + ["id"] + ); + if (!pickings.length) { + return false; + } + navigate("picking", {pickingId: pickings[0].id}); + return true; + }, + }, + {sequence: 10} +); + +barcodeScanHandlers.add( + "product", + { + async handle(barcode, parsed, {api, navigate}) { + const productCode = parsed?.value || barcode; + const productDomain = barcodeMatchDomain(productCode); + const products = productDomain + ? await api.searchRead("product.product", productDomain, [ + "id", + "display_name", + ]) + : []; + if (!products.length) { + return false; + } + navigate("quick_info", {result: products[0], result_type: "product"}); + return true; + }, + }, + {sequence: 20} +); + +barcodeScanHandlers.add( + "location", + { + async handle(barcode, parsed, {api, navigate}) { + const locationDomain = barcodeMatchDomain(barcode); + const locations = locationDomain + ? await api.searchRead("stock.location", locationDomain, [ + "id", + "display_name", + ]) + : []; + if (!locations.length) { + return false; + } + navigate("quick_info", {result: locations[0], result_type: "location"}); + return true; + }, + }, + {sequence: 30} +); diff --git a/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js b/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js new file mode 100644 index 0000000..74d1237 --- /dev/null +++ b/barcode_stock/static/src/js/screens/internal_transfer_screen.esm.js @@ -0,0 +1,478 @@ +import {Component, onWillStart, onWillUpdateProps, useState} from "@odoo/owl"; +import { + barcodeMatchAnyDomain, + barcodeMatchDomain, +} from "@barcode_scanner/js/utils/scan_match.esm"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class InternalTransferScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.notification = useService("notification"); + this.barcodeScannerState = useService("barcodeScannerState"); + const params = this.props.params || {}; + const initialLines = JSON.parse(JSON.stringify(params.lines || [])); + for (const line of initialLines) { + if (!line.lots) { + line.lots = []; + } + } + this.state = useState({ + origin_location: params.origin_location || null, + destination_location: params.destination_location || null, + responsible: params.responsible || null, + lines: initialLines, + picking_id: null, + isCheckingAvailability: false, + isValidating: false, + }); + useBarcodeHandler({ + onScan: (barcode, parsedData) => { + this.onBarcodeScanned(barcode, parsedData); + }, + }); + onWillStart(async () => { + await this.loadLotsForTrackedLines(); + }); + onWillUpdateProps(async (nextProps) => { + const nextParams = nextProps.params || {}; + const originChanged = + nextParams.origin_location?.id !== this.state.origin_location?.id; + this.state.origin_location = nextParams.origin_location || null; + this.state.destination_location = nextParams.destination_location || null; + this.state.responsible = nextParams.responsible || null; + const nextLines = JSON.parse(JSON.stringify(nextParams.lines || [])); + for (const line of nextLines) { + if (!line.lots) { + line.lots = []; + } + } + this.state.lines = nextLines; + if ( + originChanged || + this.state.lines.some((line) => line.tracking !== "none") + ) { + await this.loadLotsForTrackedLines(); + } + }); + } + + async onBarcodeScanned(barcode, parsedData) { + await this.handleBarcode(barcode, parsedData); + } + + async handleBarcode(barcode, parsedData) { + // Tolerant match: a physical reader may add whitespace or change case, + // which a raw "=" would miss (it fell through to product lookup and + // reported "not found") while a manual paste worked. + const locationDomain = barcodeMatchDomain(barcode); + const locations = locationDomain + ? await this.inventory.searchRead("stock.location", locationDomain, [ + "display_name", + ]) + : []; + if (locations.length) { + const location = locations[0]; + if (this.state.origin_location) { + this.state.destination_location = { + id: location.id, + display_name: location.display_name, + }; + this.notification.add( + _t("Destination location selected: %(location)s", { + location: location.display_name, + }), + {type: "success"} + ); + } else { + this.state.origin_location = { + id: location.id, + display_name: location.display_name, + }; + this.notification.add( + _t("Origin location selected: %(location)s", { + location: location.display_name, + }), + {type: "success"} + ); + } + return; + } + const productCode = parsedData?.value || barcode; + if (!productCode) { + this.notification.add(_t("Barcode not recognized."), {type: "warning"}); + return; + } + await this.addScannedProduct(productCode, parsedData); + } + + async addScannedProduct(productCode, parsedData = null) { + // A GS1 GTIN has several equivalent forms; the product may be stored + // under any of them, so match every candidate (like the picking screen). + const candidates = [ + ...(parsedData?.productCodes || []), + parsedData?.value, + productCode, + ]; + const productDomain = barcodeMatchAnyDomain(candidates); + const products = productDomain + ? await this.inventory.searchRead("product.product", productDomain) + : []; + if (!products.length) { + this.notification.add(_t("Product not found."), { + type: "warning", + }); + return; + } + const product = products[0]; + // The scan carries its own quantity and lot when the barcode states them. + const qty = this.barcodeScannerState.scannedQuantity( + parsedData, + product.uom_id?.[0] || null + ); + const lotName = parsedData?.lot || parsedData?.serial || null; + const lot = + lotName && product.tracking !== "none" + ? await this.findAvailableLot(product.id, lotName) + : null; + if (lotName && !lot) { + this.notification.add( + _t("Lot %(lot)s is not available here.", {lot: lotName}), + { + type: "warning", + } + ); + } + await this.addLine(product, lot?.id || null, lot?.name || null, qty); + } + + async findAvailableLot(productId, lotName) { + const wanted = lotName.trim().toUpperCase(); + const match = (lots) => + lots.find((lot) => (lot.name || "").toUpperCase() === wanted) || null; + // Prefer a lot actually on hand at the origin... + const atOrigin = match((await this.fetchLots(productId)) || []); + if (atOrigin) { + return atOrigin; + } + // ...otherwise still resolve the scanned lot against the product's + // existing lots so it lands on the line instead of being dropped; + // CHECK/VALIDATE then report whether it is available at the origin. + return match((await this.fetchAllLots(productId)) || []); + } + + async addLine(product, lotId, lotName, qty) { + const existing = this.state.lines.find( + (l) => l.product_id === product.id && l.lot_id === lotId + ); + if (existing) { + existing.qty += qty; + if (existing.tracking !== "none") { + if (!existing.lots) { + existing.lots = []; + } + if (!existing.lots.length) { + existing.lots = (await this.fetchLots(product.id)) || []; + } + } + return; + } + const lots = + product.tracking === "none" ? [] : (await this.fetchLots(product.id)) || []; + this.state.lines.push({ + product_id: product.id, + product_name: product.name, + qty: qty, + tracking: product.tracking, + lot_id: lotId, + lot_name: lotName, + lots: lots, + }); + } + + async fetchAllLots(productId) { + return ( + (await this.inventory.searchRead( + "stock.lot", + [["product_id", "=", productId]], + ["id", "name"] + )) || [] + ); + } + + async fetchLots(productId) { + if (!this.state.origin_location?.id) { + return this.fetchAllLots(productId); + } + const quants = await this.inventory.searchRead( + "stock.quant", + [ + ["product_id", "=", productId], + ["location_id", "child_of", this.state.origin_location.id], + ["quantity", ">", 0], + ["lot_id", "!=", false], + ], + ["lot_id"] + ); + const lotIds = [ + ...new Set(quants.map((quant) => quant.lot_id?.[0]).filter(Boolean)), + ]; + if (!lotIds.length) { + // Nothing lotted on hand at the origin yet: still offer the + // product's existing lots so the operator can pick one by hand + // (and a scanned lot resolves) instead of facing an empty list. + // Availability is verified later by CHECK / VALIDATE. + return this.fetchAllLots(productId); + } + return (await this.inventory.read("stock.lot", lotIds, ["name"])) || []; + } + + async loadLotsForTrackedLines() { + for (const line of this.state.lines) { + if (line.tracking !== "none") { + if (!line.lots) { + line.lots = []; + } + if (!line.lots.length) { + line.lots = (await this.fetchLots(line.product_id)) || []; + } + this.updateLineLotName(line); + } + } + } + + updateLineLotName(line) { + if (!line.lot_id) { + line.lot_name = null; + return; + } + const normalizedLotId = parseInt(line.lot_id, 10); + line.lot_id = Number.isNaN(normalizedLotId) ? line.lot_id : normalizedLotId; + const selectedLot = (line.lots || []).find((lot) => lot.id === line.lot_id); + if (selectedLot) { + line.lot_name = selectedLot.name; + } + } + + selectOriginLocation() { + this.props.navigate("location_selector", { + title: _t("Select origin location"), + type: "origin_location", + origin_location: this.state.origin_location, + destination_location: this.state.destination_location, + responsible: this.state.responsible, + lines: this.state.lines, + }); + } + + selectDestinationLocation() { + this.props.navigate("location_selector", { + title: _t("Select destination location"), + type: "destination_location", + origin_location: this.state.origin_location, + destination_location: this.state.destination_location, + responsible: this.state.responsible, + lines: this.state.lines, + }); + } + + selectResponsible() { + this.props.navigate("user_selector", { + returnRoute: "internal_transfer", + returnParams: { + origin_location: this.state.origin_location, + destination_location: this.state.destination_location, + responsible: this.state.responsible, + lines: this.state.lines, + }, + responsible: this.state.responsible, + }); + } + + addProduct() { + this.props.navigate("product_selector", { + origin_location: this.state.origin_location, + destination_location: this.state.destination_location, + responsible: this.state.responsible, + lines: this.state.lines, + }); + } + + removeLine(line) { + const index = this.state.lines.indexOf(line); + if (index !== -1) { + this.state.lines.splice(index, 1); + } + } + + get normalizedLines() { + return this.state.lines.map((line) => { + const normalizedLotId = line.lot_id ? parseInt(line.lot_id, 10) : false; + const lots = line.lots || []; + const selectedLot = lots.find((lot) => lot.id === normalizedLotId); + return { + product_id: line.product_id, + qty: parseFloat(line.qty) || 0, + lot_id: normalizedLotId || false, + lot_name: selectedLot?.name || line.lot_name || false, + }; + }); + } + + validateForm() { + if (!this.state.origin_location?.id) { + this.notification.add(_t("Please select an origin location."), { + type: "warning", + }); + return false; + } + if (!this.state.destination_location?.id) { + this.notification.add(_t("Please select a destination location."), { + type: "warning", + }); + return false; + } + if (this.state.origin_location.id === this.state.destination_location.id) { + this.notification.add( + _t("Origin and destination locations must be different."), + {type: "warning"} + ); + return false; + } + if (!this.state.lines.length) { + this.notification.add(_t("Add at least one product."), {type: "warning"}); + return false; + } + for (const line of this.state.lines) { + if (!(parseFloat(line.qty) > 0)) { + this.notification.add( + _t("Quantity must be greater than zero for %(product)s.", { + product: line.product_name, + }), + {type: "warning"} + ); + return false; + } + this.updateLineLotName(line); + if (line.tracking !== "none" && !line.lot_id) { + this.notification.add( + _t("Please select a lot/serial number for %(product)s.", { + product: line.product_name, + }), + {type: "warning"} + ); + return false; + } + } + return true; + } + + async checkAvailability() { + if (!this.validateForm()) { + return; + } + this.state.isCheckingAvailability = true; + try { + const result = await this.inventory.call( + "stock.picking", + "action_barcode_scanner_check_availability", + [ + this.state.origin_location.id, + this.state.destination_location.id, + this.normalizedLines, + this.state.responsible?.id || false, + ] + ); + const availabilityMap = {}; + for (const aline of result.lines || []) { + const key = `${aline.product_id}_${aline.lot_id || ""}`; + availabilityMap[key] = aline; + } + for (const line of this.state.lines) { + const normalizedLotId = line.lot_id ? parseInt(line.lot_id, 10) : false; + const key = `${line.product_id}_${normalizedLotId || ""}`; + const match = availabilityMap[key]; + if (match) { + line.available_qty = match.available_qty; + line.is_available = match.available; + } + } + const unavailableLines = (result.lines || []).filter( + (line) => !line.available + ); + if (!unavailableLines.length) { + this.notification.add(_t("All products are available."), { + type: "success", + }); + return; + } + const message = unavailableLines + .map( + (line) => + `${line.product_name}: ${line.available_qty}/${line.required_qty}` + ) + .join(" | "); + this.notification.add(message, {type: "warning"}); + } catch (error) { + const message = + error?.data?.message || + error?.message || + _t("Availability check failed."); + this.notification.add(message, {type: "danger"}); + } finally { + this.state.isCheckingAvailability = false; + } + } + + async validateTransfer() { + if (!this.validateForm()) { + return; + } + this.state.isValidating = true; + try { + const result = await this.inventory.call( + "stock.picking", + "action_barcode_scanner_internal_transfer", + [ + this.state.origin_location.id, + this.state.destination_location.id, + this.state.responsible?.id || false, + this.normalizedLines, + ] + ); + this.state.picking_id = result.picking_id; + this.notification.add( + _t("Internal transfer %(name)s validated successfully.", { + name: result.picking_name, + }), + {type: "success"} + ); + this.store.goBack(); + } catch (error) { + const message = + error?.data?.message || + error?.message || + _t("Internal transfer failed."); + this.notification.add(message, {type: "danger"}); + } finally { + this.state.isValidating = false; + } + } + + onLotChanged(line, ev) { + line.lot_id = ev.target.value ? parseInt(ev.target.value, 10) : false; + this.updateLineLotName(line); + } + + goBack() { + this.store.goBack(); + } +} +InternalTransferScreen.template = "barcode_scanner.InternalTransferScreen"; + +barcodeScreens.add("internal_transfer", {component: InternalTransferScreen}); diff --git a/barcode_stock/static/src/js/screens/location_selector_screen.esm.js b/barcode_stock/static/src/js/screens/location_selector_screen.esm.js new file mode 100644 index 0000000..aa3cb0b --- /dev/null +++ b/barcode_stock/static/src/js/screens/location_selector_screen.esm.js @@ -0,0 +1,115 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeMatchDomain} from "@barcode_scanner/js/utils/scan_match.esm"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class LocationSelectorScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.state = useState({ + locations: [], + search: "", + loading: true, + selectedLocation: null, + }); + + useBarcodeHandler({ + onScan: async (barcode, parsedData) => { + await this.onBarcodeScanned(barcode, parsedData); + }, + }); + + onWillStart(async () => { + await this.loadLocations(); + }); + } + + async onBarcodeScanned(barcode) { + const match = barcodeMatchDomain(barcode); + // Keep a scanned location within the caller's scope too, so scanning a + // location the operation can't use is rejected just like typing it. + const scope = this.props.params?.locationDomain || []; + const domain = match ? [...match, ...scope] : null; + const locations = domain + ? await this.inventory.searchRead("stock.location", domain, [ + "id", + "display_name", + "usage", + ]) + : []; + if (locations.length) { + this.state.selectedLocation = locations[0]; + this.confirmSelection(); + return; + } + // No location carries this barcode: filter the list by it and say so, + // instead of silently doing nothing. + this.state.search = barcode; + this.inventory.notify(_t("No location matches “%(code)s”.", {code: barcode}), { + type: "warning", + }); + } + + async loadLocations() { + // When the caller scopes the picker (e.g. the move wizard restricts the + // destination to valid locations of the operation, like the back office), + // honour that domain; otherwise show all internal locations. + const domain = this.props.params?.locationDomain || [ + ["usage", "=", "internal"], + ]; + this.state.locations = await this.inventory.searchRead( + "stock.location", + domain, + ["id", "display_name", "usage"] + ); + this.state.loading = false; + } + + get filteredLocations() { + if (!this.state.search) { + return this.state.locations; + } + const search = this.state.search.toLowerCase(); + return this.state.locations.filter((loc) => + loc.display_name.toLowerCase().includes(search) + ); + } + + selectLocation(loc) { + this.state.selectedLocation = loc; + } + + confirmSelection() { + const location = this.state.selectedLocation; + if (!location) return; + + if (this.props.params.mode === "quick_info_location") { + this.store.goBack({ + result: location, + result_type: "location", + }); + return; + } + + this.store.goBack({ + ...this.props.params, + [this.props.params.type]: location, + }); + } + + updateSearch(ev) { + this.state.search = ev.target.value; + } + + goBack() { + this.store.goBack(); + } +} + +LocationSelectorScreen.template = "barcode_scanner.LocationSelectorScreen"; + +barcodeScreens.add("location_selector", {component: LocationSelectorScreen}); diff --git a/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js b/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js new file mode 100644 index 0000000..2a93c67 --- /dev/null +++ b/barcode_stock/static/src/js/screens/move_wizard_screen.esm.js @@ -0,0 +1,672 @@ +import {Component, onMounted, onWillStart, onWillUnmount, useState} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; +import {user} from "@web/core/user"; + +export class MoveWizardScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.barcodeScannerState = useService("barcodeScannerState"); + this.barcodeScannerSync = useService("barcodeScannerSync"); + this.feedback = useService("barcodeScannerFeedback"); + this.onLotChange = this.onLotChange.bind(this); + this.setMode = this.setMode.bind(this); + this.confirmPick = this.confirmPick.bind(this); + this.goBack = this.goBack.bind(this); + this.adjustQty = this.adjustQty.bind(this); + this.setQty = this.setQty.bind(this); + this.fulfillRemaining = this.fulfillRemaining.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.selectSourceLocation = this.selectSourceLocation.bind(this); + this.selectDestLocation = this.selectDestLocation.bind(this); + + this.state = useState({ + move: null, + moveLines: [], + selectedLotId: this.props.params?.defaultLot + ? String(this.props.params.defaultLot) + : null, + qtyPicked: this.props.params?.defaultQty || 1, + loading: true, + tracking: "none", + mode: + this.props.params?.createLot && this.isIncoming ? "create_lot" : "pick", + newLotName: this.props.params?.lotName || "", + newLotExpirationDate: this.props.params?.expiration || "", + lots: [], + useExistingLots: true, + useCreateLots: true, + // Editable source/destination. Carried across the location-picker + // round-trip via params (the wizard remounts on return); otherwise + // seeded from the move in loadData. + sourceLocation: this.props.params?.moveSourceLocation || null, + destLocation: this.props.params?.moveDestLocation || null, + }); + + useBarcodeHandler({ + onScan: async (barcode, parsedData) => { + await this.onBarcodeScanned(barcode, parsedData); + }, + }); + + onWillStart(async () => { + await this.loadData(); + }); + + onMounted(() => { + document.addEventListener("keydown", this.onKeyDown); + }); + + onWillUnmount(() => { + document.removeEventListener("keydown", this.onKeyDown); + }); + } + + async onBarcodeScanned(barcode, parsedData) { + const normalized = this.barcodeScannerState.applyScanResult({ + barcode, + ...(parsedData || {}), + }); + if (!normalized.product) { + return; + } + const product = this.barcodeScannerState.getProductByBarcode( + normalized.product + ); + if (!product) { + this.inventory.notify( + _t("Scanned barcode is not a product of this operation."), + {type: "warning"} + ); + return; + } + if (product.id !== this.state.move?.product_id?.[0]) { + this.onProductScanned(product); + return; + } + this.applyScannedLot(normalized); + this.adjustQty(normalized.quantity); + this.feedback.success(); + } + + /** + * Apply the lot or serial a scan carries: select it when it already exists, + * otherwise prefill the create-lot form where creating one is allowed. + */ + applyScannedLot({lot, lotName, expiration}) { + const name = lot?.name || lotName; + if (!name || !this.isTracked) { + return; + } + if (lot && this.canUseExistingLot) { + if (!this.state.lots.some((item) => item.id === lot.id)) { + this.state.lots = [lot, ...this.state.lots]; + } + this.state.selectedLotId = String(lot.id); + return; + } + if (!this.canCreateLot) { + this.inventory.notify( + _t("Lot %(lot)s is not available for this product.", {lot: name}), + {type: "warning"} + ); + return; + } + this.state.newLotName = name; + if (expiration) { + this.state.newLotExpirationDate = expiration; + } + this.setMode("create_lot"); + } + + get isIncoming() { + return this.props.params?.pickingTypeCode === "incoming"; + } + + get isOutgoing() { + return this.props.params?.pickingTypeCode === "outgoing"; + } + + /** + * Let the operator choose where stock comes from / goes to. Enabled for + * receptions (put-away destination) and internal transfers; kept off for + * deliveries, whose destination is the customer location and must not be + * repointed to an internal one from here. + */ + get canEditLocations() { + return this.props.params?.pickingTypeCode !== "outgoing"; + } + + get moveId() { + return this.props.params?.moveId; + } + + get pickingId() { + return this.props.params?.pickingId; + } + + get listParams() { + return this.props.params?.listParams || null; + } + + get demandQty() { + return this.state.move?.quantity || this.state.move?.product_uom_qty || 0; + } + + get doneQty() { + return Math.max(this.demandQty - this.remainingQty, 0); + } + + get remainingQty() { + return this.barcodeScannerState.getRemainingQty(this.moveId); + } + + get canDecrement() { + if (this.isSerial) return false; + return (this.state.qtyPicked || 0) > 0; + } + + get canIncrement() { + if (this.isSerial) { + return this.state.qtyPicked < 1 && !this.selectedLotIdNumber; + } + // Over-picking is allowed (Odoo puts no cap on done vs demand), so the + // stepper can always add; the remaining figure stays informational. + return true; + } + + get canFulfill() { + if (this.isSerial) return false; + return this.remainingQty - (this.state.qtyPicked || 0) > 1; + } + + get fulfillIncrement() { + return Math.max(this.remainingQty - (this.state.qtyPicked || 0), 0); + } + + get isTracked() { + return this.state.tracking !== "none"; + } + + get hasLotGroup() { + return user.hasGroup("stock.group_production_lot"); + } + + get isSerial() { + return this.state.tracking === "serial"; + } + + get canCreateLot() { + return this.isIncoming && this.isTracked && this.state.useCreateLots; + } + + get canUseExistingLot() { + return this.isTracked && this.state.useExistingLots; + } + + get selectedLotIdNumber() { + const lotId = parseInt(this.state.selectedLotId, 10); + return Number.isNaN(lotId) ? null : lotId; + } + + get selectedLot() { + return ( + this.state.lots.find((lot) => lot.id === this.selectedLotIdNumber) || null + ); + } + + get realLots() { + return (this.state.lots || []).filter((lot) => lot.id > 0); + } + + get headerEyebrow() { + return this.isIncoming ? _t("Reception task") : _t("Picking task"); + } + + get headerSubtitle() { + if (this.state.mode === "create_lot") { + return _t("Create the lot first, then confirm the move in one pass."); + } + if (this.isTracked) { + return _t("Select the correct lot or serial and confirm the quantity."); + } + return _t("Confirm the quantity and return to the picking queue."); + } + + get trackingLabel() { + const labels = { + none: _t("No tracking"), + lot: _t("Lot tracked"), + serial: _t("Serial tracked"), + }; + return labels[this.state.tracking] || _t("Tracking"); + } + + get lotFieldLabel() { + return this.isSerial ? _t("Serial number") : _t("Lot number"); + } + + get quantityHelper() { + if (this.isSerial) { + return _t("Serial-tracked products can only process one unit."); + } + return `${this.remainingQty} ${_t("remaining to process")}`; + } + + get confirmLabel() { + return this.state.mode === "create_lot" + ? _t("Create lot & confirm") + : _t("Confirm move"); + } + + get backLabel() { + return this.state.mode === "create_lot" ? _t("Back to pick") : _t("Back"); + } + + get modeDescription() { + return this.state.mode === "create_lot" + ? _t("Register a new lot before assigning the picked quantity.") + : _t("Use an existing lot or serial already available for this product."); + } + + get selectedLotExpirationLabel() { + return this.selectedLot?.expiration_date || _t("No expiration date"); + } + + get selectedLotQtyLabel() { + const qty = this.selectedLot?.product_qty; + if (qty === undefined || qty === null || qty === false) { + return _t("Qty unavailable"); + } + return `${qty} ${_t("on hand")}`; + } + + get quantityInputMax() { + return this.isSerial ? 1 : undefined; + } + + get todayDate() { + return new Date().toISOString().split("T")[0]; + } + + getTodayStart() { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; + } + + isLotExpired(lot) { + if (!lot?.expiration_date) { + return false; + } + const expiration = new Date(lot.expiration_date); + expiration.setHours(0, 0, 0, 0); + return expiration < this.getTodayStart(); + } + + setMode(mode) { + if (mode === "create_lot" && !this.canCreateLot) { + return; + } + this.state.mode = mode; + } + + adjustQty(delta) { + if (this.state.loading) return; + const next = (parseFloat(this.state.qtyPicked) || 0) + delta; + if (this.isSerial) { + this.state.qtyPicked = Math.max(0, Math.min(1, next)); + } else { + this.state.qtyPicked = Math.max(0, Math.min(this.remainingQty, next)); + } + this.state._lastInputSource = "tap"; + } + + /** + * A quantity that arrives prefilled (a count carried by the scanned barcode) + * still has to respect the serial rule, but it is NOT capped to the + * remaining demand: over-picking is allowed, exactly as Odoo lets you + * receive/process more than was ordered. + */ + clampScannedQty(value) { + const qty = parseFloat(value) || 1; + if (this.isSerial) { + return 1; + } + return qty; + } + + setQty(value) { + const qty = parseFloat(value); + if (Number.isNaN(qty) || qty < 0) { + this.state.qtyPicked = 0; + return; + } + // Serial stays a single unit; anything else may exceed the demand + // (over-picking is allowed, matching Odoo). + this.state.qtyPicked = this.isSerial ? Math.min(qty, 1) : qty; + this.state._lastInputSource = "tap"; + } + + fulfillRemaining() { + if (!this.canFulfill) return; + this.state.qtyPicked = this.remainingQty; + this.state._lastInputSource = "tap"; + this.feedback.success({ + message: _t("Remaining quantity applied"), + notify: true, + }); + } + + onProductScanned(product) { + if (!product) return; + const moveProductId = this.state.move?.product_id?.[0]; + if (product.id === moveProductId) { + this.feedback.success(); + return; + } + this.inventory.notify( + _t( + "Scanned product doesn't match this move (%(expected)s expected, got %(got)s)", + { + expected: this.state.move?.product_id?.[1] || "", + got: product.display_name, + } + ), + {type: "warning"} + ); + this.feedback.warning(); + } + + onKeyDown(ev) { + if (ev.target?.matches?.("input,textarea,[contenteditable]")) { + return; + } + if (ev.key === "ArrowUp") { + ev.preventDefault(); + this.adjustQty(1); + } else if (ev.key === "ArrowDown") { + ev.preventDefault(); + this.adjustQty(-1); + } else if (ev.key === "f" || ev.key === "F") { + if (this.canFulfill) { + ev.preventDefault(); + this.fulfillRemaining(); + } + } + } + + onLotChange(ev) { + this.state.selectedLotId = ev.target.value || null; + } + + /** + * Open the shared location picker, forwarding everything the wizard needs + * to rebuild itself on return (it remounts) so the in-progress quantity, + * lot and mode survive, plus both current location choices and which field + * this selection targets. The picker writes the chosen location back under + * `type` and returns here. + */ + navigateToLocationSelector(type, locationDomain = null) { + this.store.navigate("location_selector", { + moveId: this.moveId, + pickingId: this.pickingId, + pickingTypeCode: this.props.params?.pickingTypeCode, + listParams: this.listParams, + defaultQty: this.state.qtyPicked, + defaultLot: this.state.selectedLotId, + lotName: this.state.newLotName, + expiration: this.state.newLotExpirationDate, + createLot: this.state.mode === "create_lot", + moveSourceLocation: this.state.sourceLocation, + moveDestLocation: this.state.destLocation, + locationDomain, + type, + }); + } + + selectSourceLocation() { + this.navigateToLocationSelector("moveSourceLocation"); + } + + selectDestLocation() { + this.navigateToLocationSelector( + "moveDestLocation", + this.locationDomainFor("location_dest_id") + ); + } + + // Restrict the destination picker to valid locations for this move -- exactly + // Odoo's back-office domain for the move line "To": child_of the picking's own + // destination, excluding view locations. Without it the picker offered every + // internal location (any warehouse), and choosing one outside the operation's + // warehouse silently reverted to the operation type default on validation. + locationDomainFor(field) { + const picking = this.barcodeScannerState.picking; + const anchor = + field === "location_dest_id" + ? picking?.location_dest_id?.[0] || + this.state.move?.location_dest_id?.[0] + : picking?.location_id?.[0] || this.state.move?.location_id?.[0]; + if (!anchor) { + return null; + } + return [ + ["id", "child_of", anchor], + ["usage", "!=", "view"], + ]; + } + + async loadData() { + if ( + !this.barcodeScannerState.ready || + this.barcodeScannerState.activePickingId !== this.pickingId + ) { + await this.barcodeScannerState.preloadPicking(this.pickingId); + } + const moveId = this.moveId; + const move = this.barcodeScannerState.getMove(moveId); + const product = this.barcodeScannerState.getProduct(move.product_id[0]); + const moveLines = this.barcodeScannerState.getMoveLinesForMove(moveId); + + this.state.useExistingLots = this.barcodeScannerState.useExistingLots; + this.state.useCreateLots = this.barcodeScannerState.useCreateLots; + + let lots = this.barcodeScannerState.getLotsForProduct(move.product_id[0], { + onlyAvailable: !this.isIncoming, + excludeExpired: false, + }); + + if (!this.state.useExistingLots) { + lots = []; + } + + const defaultLotId = this.selectedLotIdNumber; + if (defaultLotId && !lots.some((lot) => lot.id === defaultLotId)) { + const defaultLot = this.barcodeScannerState.lotsById[defaultLotId]; + if (defaultLot) { + lots = [defaultLot, ...lots]; + } + } + + this.state.move = move; + this.state.moveLines = moveLines; + this.state.tracking = product.tracking; + + // Seed the editable locations from the move unless the operator already + // picked one (preserved across the location-picker round-trip). + if (!this.state.sourceLocation && move.location_id) { + this.state.sourceLocation = { + id: move.location_id[0], + display_name: move.location_id[1], + }; + } + if (!this.state.destLocation && move.location_dest_id) { + this.state.destLocation = { + id: move.location_dest_id[0], + display_name: move.location_dest_id[1], + }; + } + + if (this.isTracked && this.isIncoming) { + if (this.state.useCreateLots && !this.state.useExistingLots) { + this.state.mode = "create_lot"; + } else if (this.state.useExistingLots && !this.state.useCreateLots) { + this.state.mode = "pick"; + } + } + this.state.lots = lots || []; + this.state.qtyPicked = this.clampScannedQty(this.state.qtyPicked); + this.state.loading = false; + } + + async ensureLotCreated() { + if (!this.state.newLotName) { + this.inventory.notify(_t("Lot name required"), {type: "danger"}); + return null; + } + if ( + this.isSerial && + this.state.lots.some( + (lot) => lot.name?.toLowerCase() === this.state.newLotName.toLowerCase() + ) + ) { + this.inventory.notify(_t("Serial already exists for this product."), { + type: "danger", + }); + return null; + } + const lot = this.barcodeScannerState.stageLotCreate({ + productId: this.state.move.product_id[0], + name: this.state.newLotName, + expirationDate: this.state.newLotExpirationDate || false, + }); + this.state.lots = [ + ...this.state.lots.filter((item) => item.id !== lot.id), + lot, + ]; + this.state.selectedLotId = String(lot.id); + return lot; + } + + async confirmPick() { + if (this.state.mode === "create_lot" && !this.isIncoming) { + this.inventory.notify(_t("Lots can only be created in receptions"), { + type: "danger", + }); + return; + } + if (this.state.mode === "create_lot" && !this.state.selectedLotId) { + const lot = await this.ensureLotCreated(); + if (!lot) { + return; + } + } + if (!this.state.qtyPicked || this.state.qtyPicked <= 0) { + this.inventory.notify(_t("Invalid quantity"), {type: "danger"}); + return; + } + if ( + this.state.tracking === "serial" && + parseFloat(this.state.qtyPicked) !== 1 + ) { + this.inventory.notify( + _t("Serial-tracked products must have a quantity of 1."), + {type: "danger"} + ); + return; + } + if ( + this.state.tracking !== "none" && + (this.canUseExistingLot || this.canCreateLot) && + !this.selectedLotIdNumber && + this.state.mode !== "create_lot" + ) { + this.inventory.notify(_t("Please select a lot"), {type: "danger"}); + return; + } + + const guardrailError = this.barcodeScannerSync.validateConfirmMovePayload({ + productId: this.state.move.product_id[0], + lotId: this.selectedLotIdNumber || false, + lotName: this.selectedLot + ? this.selectedLot.name + : this.state.newLotName || false, + qtyPicked: this.state.qtyPicked, + createLot: this.state.mode === "create_lot", + expirationDate: this.state.newLotExpirationDate || false, + moveId: this.moveId, + }); + if (guardrailError) { + this.inventory.notify(guardrailError.message, {type: "danger"}); + return; + } + const lot = this.selectedLot; + // Receptions and internal moves may use an expired lot (it records what + // arrived / relocates it); only a delivery to a customer is blocked. + if (this.isLotExpired(lot) && this.isOutgoing) { + this.inventory.notify(_t("Lot expired"), {type: "danger"}); + return; + } + + await this.barcodeScannerSync.confirmMove({ + moveId: this.moveId, + pickingId: this.pickingId, + productId: this.state.move.product_id[0], + qtyPicked: this.state.qtyPicked, + lotId: this.selectedLotIdNumber || false, + lotName: lot ? lot.name : this.state.newLotName || false, + createLot: this.state.mode === "create_lot", + expirationDate: this.state.newLotExpirationDate || false, + locationId: this.state.sourceLocation?.id || this.state.move.location_id[0], + locationDestId: + this.state.destLocation?.id || this.state.move.location_dest_id[0], + }); + // Hand the move back so the picking screen opens on the tab that + // now holds it: still "To Do" while quantity is missing, "Done" once + // this confirmation completed it. + this.store.navigate( + "picking", + { + pickingId: this.pickingId, + listParams: this.listParams, + reloadToken: Date.now(), + focusMoveId: this.moveId, + }, + {replace: true} + ); + } + + async goBack() { + if (this.state.mode === "create_lot" && this.canUseExistingLot) { + this.setMode("pick"); + return; + } + + this.store.navigate( + "picking", + { + pickingId: this.pickingId, + listParams: this.listParams, + reloadToken: Date.now(), + }, + {replace: true} + ); + } + + toggleCreateLot() { + this.setMode(this.state.mode === "pick" ? "create_lot" : "pick"); + } +} + +MoveWizardScreen.template = "barcode_scanner.MoveWizardScreen"; +MoveWizardScreen.props = { + params: {type: Object, optional: true}, + navigate: {type: Function, optional: true}, +}; + +barcodeScreens.add("move_wizard", {component: MoveWizardScreen}); diff --git a/barcode_stock/static/src/js/screens/picking_list_screen.esm.js b/barcode_stock/static/src/js/screens/picking_list_screen.esm.js new file mode 100644 index 0000000..76a8cf3 --- /dev/null +++ b/barcode_stock/static/src/js/screens/picking_list_screen.esm.js @@ -0,0 +1,982 @@ +import { + Component, + onWillStart, + useEffect, + useExternalListener, + useState, +} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {deserializeDateTime} from "@web/core/l10n/dates"; +import {scanBarcode} from "@web/core/barcode/barcode_dialog"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; +import {user} from "@web/core/user"; + +const GROUP_ORDER = ["date", "state"]; +const FILTER_ORDER = ["state", "date"]; +const FILTER_DEFAULTS = {state: "all", date: null}; +const FILTER_LABELS = {state: _t("Status"), date: _t("Date")}; + +const EMPTY_MOVE_STATS = { + skuCount: 0, + demandQty: 0, + doneQty: 0, + remainingQty: 0, +}; + +const OPERATION_LABELS = { + incoming: _t("Receipts"), + internal: _t("Internal Transfers"), + outgoing: _t("Delivery Orders"), +}; + +// In-session working view (filters + grouping) per operation-type code. Module +// scope so it outlives the screen being re-created on navigation: entering a +// picking and coming back keeps the filter the user was working with, without +// persisting it to the server (that is what the ★ default is for). Cleared only +// on a full app reload. +const SESSION_VIEWS = {}; + +export class PickingListScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.feedback = useService("barcodeScannerFeedback"); + this.state = useState({ + pickings: [], + moveStatsByPickingId: {}, + groupedPickings: {}, + loading: true, + groupByLevels: [], + collapsedGroups: {}, + search: "", + stateLabels: {}, + activeFilters: [], + filterValues: {state: "all", date: null}, + savedDefault: null, + openMenu: null, + }); + // Whole `res.users.barcode_default_filters` map, kept out of reactive + // state: only this operation type's entry (state.savedDefault) drives the + // ★, but the full map must survive a write so other operations' defaults + // are not wiped. + this.allDefaults = {}; + this.groupLabels = { + state: _t("Status"), + date: _t("Date"), + }; + this.filterLabels = FILTER_LABELS; + this.groupOrder = GROUP_ORDER; + + // Close any open filter/group menu when tapping outside the toolbar; the + // toolbar itself stops propagation so its own taps do not trigger this. + useExternalListener(document, "click", () => this.closeMenu()); + + useBarcodeHandler({ + onScan: (barcode) => this.onBarcodeScanned(barcode), + }); + + onWillStart(async () => { + await this.loadStateLabels(); + await this.loadDefaultFilter(); + await this.loadPickings(); + }); + + this.openPicking = (id) => { + this.store.navigate("picking", { + pickingId: id, + listParams: this.props.params, + }); + }; + + useEffect( + () => { + this.computeGroups(); + this.persistSessionView(); + const collapsed = {}; + const groups = this.state.groupedPickings; + const keys = Object.keys(groups); + keys.forEach((key, index) => { + collapsed["root-" + key] = this.state.search ? false : index !== 0; + }); + this.state.collapsedGroups = collapsed; + }, + () => [ + this.state.pickings, + this.state.groupByLevels, + this.state.search, + this.state.activeFilters, + this.state.filterValues.state, + this.state.filterValues.date, + ] + ); + } + + async loadPickings() { + const type = this.props.params?.type; + const warehouseId = this.props.params?.warehouseId; + const domain = [ + ["picking_type_id.warehouse_id", "=", warehouseId], + ["picking_type_id.code", "=", type], + ["state", "not in", ["done", "cancel"]], + ]; + const fields = ["name", "partner_id", "scheduled_date", "state"]; + const result = await this.inventory.searchRead( + "stock.picking", + domain, + fields, + { + order: "scheduled_date asc", + } + ); + this.state.moveStatsByPickingId = await this.loadMoveStats(result); + this.state.pickings = result; + this.state.collapsedGroups = {}; + this.computeGroups(); + this.state.loading = false; + } + + async loadMoveStats(pickings) { + const pickingIds = pickings.map((picking) => picking.id).filter(Boolean); + if (!pickingIds.length) { + return {}; + } + const moves = await this.inventory.searchRead( + "stock.move", + [ + ["picking_id", "in", pickingIds], + ["product_uom_qty", ">", 0], + ], + ["picking_id", "product_uom_qty", "qty_done_total", "quantity"] + ); + const stats = {}; + for (const move of moves) { + const pickingId = move.picking_id?.[0]; + if (!pickingId) { + continue; + } + stats[pickingId] = stats[pickingId] || {...EMPTY_MOVE_STATS}; + stats[pickingId].skuCount += 1; + stats[pickingId].demandQty += move.product_uom_qty || 0; + stats[pickingId].doneQty += move.qty_done_total || 0; + stats[pickingId].remainingQty += Math.max( + (move.quantity || move.product_uom_qty || 0) - + (move.qty_done_total || 0), + 0 + ); + } + return stats; + } + + getGroupKey(p, groupBy) { + if (groupBy === "state") { + return this.state.stateLabels[p.state] || _t("Unknown"); + } + if (groupBy === "date") { + return p.scheduled_date + ? this.getRelativeDayLabel(this.parseScheduledDate(p.scheduled_date)) + : _t("No date"); + } + return _t("Other"); + } + + groupRecursively(records, levels) { + if (!levels.length) { + return records; + } + const level = levels[0]; + const rest = levels.slice(1); + const groups = {}; + for (const r of records) { + const key = this.getGroupKey(r, level); + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(r); + } + for (const key in groups) { + groups[key] = this.groupRecursively(groups[key], rest); + } + return groups; + } + + goBack() { + this.store.goBack(); + } + + get filteredPickings() { + return this.getMatchingPickings(this.state.search); + } + + computeGroups() { + if (!this.state.groupByLevels.length) { + this.state.groupedPickings = {all: this.filteredPickings}; + return; + } + this.state.groupedPickings = this.groupRecursively( + this.filteredPickings, + this.state.groupByLevels + ); + } + + toggleGroupBy(group, ev) { + if (ev.target.checked) { + if (!this.state.groupByLevels.includes(group)) { + this.state.groupByLevels = GROUP_ORDER.filter((g) => + g === group + ? ev.target.checked + : this.state.groupByLevels.includes(g) + ); + } + } else { + this.state.groupByLevels = this.state.groupByLevels.filter( + (g) => g !== group + ); + } + this.computeGroups(); + } + + toggleGroup(path) { + this.state.collapsedGroups[path] = !this.state.collapsedGroups[path]; + } + + addGroup(ev) { + const value = ev.target.value; + if (!value) { + return; + } + if (!this.state.groupByLevels.includes(value)) { + this.state.groupByLevels = GROUP_ORDER.filter( + (group) => group === value || this.state.groupByLevels.includes(group) + ); + } + this.computeGroups(); + ev.target.value = ""; + } + + removeGroup(group) { + this.state.groupByLevels = this.state.groupByLevels.filter((g) => g !== group); + this.computeGroups(); + } + + async loadStateLabels() { + const fields = await this.inventory.call( + "stock.picking", + "fields_get", + [["state"]], + { + attributes: ["selection"], + } + ); + const selection = fields.state.selection; + const labelsMap = {}; + for (const [key, label] of selection) { + labelsMap[key] = label; + } + this.state.stateLabels = labelsMap; + } + + get operationLabel() { + return OPERATION_LABELS[this.props.params?.type] || _t("Pickings"); + } + + get headerSubtitle() { + if (this.state.loading) { + return _t("Preparing scanner-first work queue..."); + } + return `${this.filteredPickings.length}/${this.state.pickings.length} ${_t( + "visible" + )} · ${this.operationLabel}`; + } + + get summaryCards() { + return [ + { + key: "open", + label: _t("Open"), + value: this.state.pickings.length, + tone: "default", + }, + { + key: "ready", + label: _t("Ready"), + value: this.state.pickings.filter((picking) => + this.isReadyPicking(picking) + ).length, + tone: "success", + }, + { + key: "visible", + label: _t("Visible"), + value: this.filteredPickings.length, + tone: "info", + }, + ]; + } + + get availableGroupingOptions() { + return GROUP_ORDER.filter((group) => !this.state.groupByLevels.includes(group)); + } + + normalizeSearchValue(value) { + return (value || "").toString().trim().toLowerCase(); + } + + parseScheduledDate(value) { + if (!value) { + return null; + } + // Scheduled_date arrives from the ORM in UTC; deserializeDateTime + // converts it to the user's timezone so that day-based grouping, the + // date filter and the Today/Tomorrow and Urgent labels line up with the + // date shown on the picking instead of slipping to the previous day. + const date = deserializeDateTime(value); + return date && date.isValid ? date.toJSDate() : null; + } + + isSameDay(first, second) { + return ( + first.getFullYear() === second.getFullYear() && + first.getMonth() === second.getMonth() && + first.getDate() === second.getDate() + ); + } + + isReadyPicking(picking) { + return picking.state === "assigned"; + } + + isUrgentPicking(picking) { + const date = this.parseScheduledDate(picking.scheduled_date); + if (!date) { + return false; + } + const endOfToday = new Date(); + endOfToday.setHours(23, 59, 59, 999); + return date <= endOfToday; + } + + getRelativeDayLabel(date) { + if (!date) { + return _t("No date"); + } + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + if (this.isSameDay(date, today)) { + return _t("Today"); + } + if (this.isSameDay(date, tomorrow)) { + return _t("Tomorrow"); + } + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }); + } + + getTimeLabel(picking) { + const date = this.parseScheduledDate(picking.scheduled_date); + if (!date) { + return _t("No time"); + } + return date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + } + + getScheduleSummary(picking) { + const date = this.parseScheduledDate(picking.scheduled_date); + if (!date) { + return _t("No schedule"); + } + return `${this.getRelativeDayLabel(date)}, ${this.getTimeLabel(picking)}`; + } + + getStateLabel(picking) { + return this.state.stateLabels[picking.state] || picking.state || _t("Unknown"); + } + + getPartnerName(picking) { + return picking.partner_id?.[1] || _t("No customer"); + } + + getMoveStats(picking) { + return this.state.moveStatsByPickingId[picking.id] || EMPTY_MOVE_STATS; + } + + getSkuSummary(picking) { + const stats = this.getMoveStats(picking); + if (!stats.skuCount) { + return _t("Queue ready"); + } + return `${stats.skuCount} ${_t("SKUs to pick")}`; + } + + getProgressLabel(picking) { + const stats = this.getMoveStats(picking); + if (!stats.skuCount) { + return ""; + } + if (stats.remainingQty <= 0) { + return _t("Completed"); + } + if (stats.doneQty > 0 && stats.demandQty > 0) { + return `${Math.round((stats.doneQty / stats.demandQty) * 100)}% ${_t( + "done" + )}`; + } + return `${Math.round(stats.remainingQty)} ${_t("remaining")}`; + } + + getStateBadgeClass(picking) { + return `ilx-state-pill ilx-state-pill--${picking.state || "default"}`; + } + + getPickingSearchText(picking) { + return [ + picking.name, + this.getPartnerName(picking), + this.getStateLabel(picking), + this.getScheduleSummary(picking), + ] + .map((value) => this.normalizeSearchValue(value)) + .join(" "); + } + + matchesSearch(picking, search) { + if (!search) { + return true; + } + return this.getPickingSearchText(picking).includes(search); + } + + matchesDateFilter(picking, dateFilter) { + if (!dateFilter) { + return true; + } + const pickingDate = this.parseScheduledDate(picking.scheduled_date); + if (!pickingDate) { + return false; + } + const today = new Date(); + if (dateFilter === "today") { + return this.isSameDay(pickingDate, today); + } + if (dateFilter === "tomorrow") { + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + return this.isSameDay(pickingDate, tomorrow); + } + if (dateFilter.startsWith("custom:")) { + const customDateStr = dateFilter.substring(7); + if (!customDateStr) { + return true; + } + const parts = customDateStr.split("-"); + const customDate = new Date( + parseInt(parts[0], 10), + parseInt(parts[1], 10) - 1, + parseInt(parts[2], 10) + ); + return this.isSameDay(pickingDate, customDate); + } + return true; + } + + getMatchingPickings(query = "") { + const search = this.normalizeSearchValue(query); + return this.state.pickings.filter((picking) => { + for (const filter of this.state.activeFilters) { + const value = this.state.filterValues[filter]; + if (filter === "state" && value !== "all" && picking.state !== value) { + return false; + } + if ( + filter === "date" && + value && + !this.matchesDateFilter(picking, value) + ) { + return false; + } + } + return this.matchesSearch(picking, search); + }); + } + + getExactScanMatch(barcode) { + const normalized = this.normalizeSearchValue(barcode); + const exactMatches = this.state.pickings.filter( + (picking) => this.normalizeSearchValue(picking.name) === normalized + ); + return exactMatches.length === 1 ? exactMatches[0] : null; + } + + async openCameraScanner() { + try { + const barcode = await scanBarcode(this.env, "environment"); + if (barcode) { + this.onBarcodeScanned(barcode); + } + } catch (error) { + const msg = error?.message || ""; + // A user-cancelled/aborted scan is expected; only surface real errors. + if (!msg.includes("cancel") && !msg.includes("abort")) { + this.feedback.error({ + notify: true, + message: _t("Could not start camera: ") + msg, + }); + } + } + } + + async onBarcodeScanned(barcode) { + const scanValue = (barcode || "").trim(); + if (!scanValue) { + return; + } + const exactMatch = this.getExactScanMatch(scanValue); + if (exactMatch) { + this.feedback.success({ + notify: true, + message: _t("Picking opened from scan."), + }); + this.openPicking(exactMatch.id); + return; + } + this.state.search = scanValue; + const matches = this.getMatchingPickings(scanValue); + if (matches.length === 1) { + this.feedback.success({ + notify: true, + message: _t("Single picking matched the scan."), + }); + this.openPicking(matches[0].id); + return; + } + if (matches.length > 1) { + this.feedback.info({ + notify: true, + message: _t("Scan narrowed the queue."), + }); + return; + } + // Not in the list currently shown here (which is scoped to this + // warehouse, operation type and open state). The scanned name may belong + // to another type, warehouse or an already-done operation, so look it up + // on the server by exact name and open it -- otherwise scanning a valid + // operation "worked for some and not others" with no clear reason. + const found = await this.inventory.searchRead( + "stock.picking", + [["name", "=", scanValue]], + ["id"], + {limit: 1} + ); + if (found.length) { + this.feedback.success({ + notify: true, + message: _t("Operation opened from scan."), + }); + this.openPicking(found[0].id); + return; + } + this.feedback.warning({ + notify: true, + message: _t("No operation found for “%(code)s”.", {code: scanValue}), + }); + } + + addFilter(ev) { + const value = ev.target.value; + if (!value || this.state.activeFilters.includes(value)) { + return; + } + this.state.activeFilters = [...this.state.activeFilters, value]; + this.state.filterValues[value] = FILTER_DEFAULTS[value]; + ev.target.value = ""; + } + + removeFilter(filter) { + this.state.activeFilters = this.state.activeFilters.filter((f) => f !== filter); + this.state.filterValues[filter] = FILTER_DEFAULTS[filter]; + this.computeGroups(); + } + + setFilterValue(filter, value) { + if (filter === "date" && value === "custom") { + this.state.filterValues.date = "custom:"; + } else { + this.state.filterValues[filter] = value; + } + this.computeGroups(); + } + + setCustomDate(dateStr) { + if (dateStr) { + this.state.filterValues.date = "custom:" + dateStr; + } else { + this.state.filterValues.date = null; + } + this.computeGroups(); + } + + toggleMenu(name) { + this.state.openMenu = this.state.openMenu === name ? null : name; + } + + closeMenu() { + if (this.state.openMenu) { + this.state.openMenu = null; + } + } + + /** Radio options for the Date section of the Filters menu. */ + get dateFilterOptions() { + return [ + {value: null, label: _t("All")}, + {value: "today", label: _t("Today")}, + {value: "tomorrow", label: _t("Tomorrow")}, + {value: "custom:", label: _t("Custom date")}, + ]; + } + + isDateOptionSelected(value) { + const active = this.state.activeFilters.includes("date"); + const current = this.state.filterValues.date; + if (!value) { + return !active; + } + if (value === "custom:") { + return ( + active && typeof current === "string" && current.startsWith("custom:") + ); + } + return active && current === value; + } + + /** Pick a status from the menu; "all" turns the status filter off. */ + setStateFilter(value) { + if (!value || value === "all") { + this.removeFilter("state"); + return; + } + if (!this.state.activeFilters.includes("state")) { + this.state.activeFilters = [...this.state.activeFilters, "state"]; + } + this.state.filterValues.state = value; + this.computeGroups(); + } + + /** Label of the "assigned" state, shown on the Ready quick filter. */ + get readyStateLabel() { + return this.state.stateLabels.assigned || _t("Ready"); + } + + /** The Ready quick filter is just a shortcut for status = assigned. */ + get isReadyQuickActive() { + return ( + this.state.activeFilters.includes("state") && + this.state.filterValues.state === "assigned" + ); + } + + toggleReadyQuick() { + this.setStateFilter(this.isReadyQuickActive ? "all" : "assigned"); + } + + /** Pick a date option from the menu; the empty value turns it off. */ + setDateFilter(value) { + if (!value) { + this.removeFilter("date"); + return; + } + if (!this.state.activeFilters.includes("date")) { + this.state.activeFilters = [...this.state.activeFilters, "date"]; + } + this.state.filterValues.date = value; + this.computeGroups(); + } + + /** Add or remove a grouping level from the Group menu, keeping GROUP_ORDER. */ + toggleGroupLevel(group) { + if (this.state.groupByLevels.includes(group)) { + this.removeGroup(group); + return; + } + this.state.groupByLevels = GROUP_ORDER.filter( + (level) => level === group || this.state.groupByLevels.includes(level) + ); + this.computeGroups(); + } + + getFilterDisplayValue(filter) { + const value = this.state.filterValues[filter]; + if (filter === "state") { + if (value === "all") { + return _t("All"); + } + return this.state.stateLabels[value] || value; + } + if (filter === "ready") { + return value ? _t("Yes") : _t("No"); + } + if (filter === "date") { + if (!value) { + return _t("All"); + } + if (value === "today") { + return _t("Today"); + } + if (value === "tomorrow") { + return _t("Tomorrow"); + } + if (value.startsWith("custom:")) { + const dateStr = value.substring(7); + if (!dateStr) { + return _t("Select date"); + } + const parts = dateStr.split("-"); + const d = new Date( + parseInt(parts[0], 10), + parseInt(parts[1], 10) - 1, + parseInt(parts[2], 10) + ); + return d.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); + } + return value; + } + return ""; + } + + get availableFilterOptions() { + return FILTER_ORDER.filter((f) => !this.state.activeFilters.includes(f)); + } + + clearFilters() { + this.state.search = ""; + this.state.activeFilters = []; + this.state.filterValues = {state: "all", date: null}; + this.computeGroups(); + } + + /** + * The operation type this list belongs to; also the key under which its + * default filter is stored in `res.users.barcode_default_filters`. + */ + get operationType() { + return this.props.params?.type || null; + } + + /** + * The built-in, Odoo-like default: show the pickings that are ready to + * process. Applied on entry when the user has not saved their own default. + */ + get builtinDefaultConfig() { + return {activeFilters: ["state"], filterValues: {state: "assigned"}}; + } + + /** + * Read the user's saved defaults and apply the one for this operation type + * before the pickings load, so the queue arrives already filtered. + */ + async loadDefaultFilter() { + if (!this.operationType) { + return; + } + let stored = {}; + try { + const [record] = await this.inventory.read( + "res.users", + [user.userId], + ["barcode_default_filters"] + ); + stored = record?.barcode_default_filters || {}; + } catch { + // A malformed map or a read hiccup must never keep the list from + // loading: fall back to no default. + stored = {}; + } + this.allDefaults = stored && typeof stored === "object" ? stored : {}; + const config = this.allDefaults[this.operationType]; + if (config && Array.isArray(config.activeFilters)) { + // Keep the saved default for the ★ state regardless of what we show. + this.state.savedDefault = this.normalizeFilterConfig(config); + } + // What to show, in priority order: + const session = SESSION_VIEWS[this.operationType]; + if (session) { + // The filter the user was working with before navigating away: + // survive the round trip into a picking so it need not be re-set. + this.applyViewState(session); + } else if (config && Array.isArray(config.activeFilters)) { + this.applyFilterConfig(config); + } else { + // No personal default: open on the ready-to-process view, the way + // Odoo opens its operation lists. A ★ default overrides this. + this.applyFilterConfig(this.builtinDefaultConfig); + } + } + + /** Restore a full working view (filters + grouping) from the session stash. */ + applyViewState(view) { + this.applyFilterConfig(view); + this.state.groupByLevels = [...(view.groupByLevels || [])]; + } + + /** + * Remember the current working view for this operation type, so it survives + * the screen being torn down and rebuilt on navigation (picking ↔ list). + */ + persistSessionView() { + if (!this.operationType) { + return; + } + SESSION_VIEWS[this.operationType] = { + activeFilters: [...this.state.activeFilters], + filterValues: {...this.state.filterValues}, + groupByLevels: [...this.state.groupByLevels], + }; + } + + /** + * Canonical, comparable form of a filter config: sorted keys and only the + * values of the active filters, so the ★ can tell "current == saved" apart + * from cosmetic ordering differences. + */ + normalizeFilterConfig({activeFilters, filterValues}) { + const filters = [...(activeFilters || [])].filter(Boolean).sort(); + const values = {}; + for (const filter of filters) { + const value = (filterValues || {})[filter]; + values[filter] = value === undefined ? FILTER_DEFAULTS[filter] : value; + } + return {activeFilters: filters, filterValues: values}; + } + + applyFilterConfig(config) { + const normalized = this.normalizeFilterConfig(config); + this.state.activeFilters = [...normalized.activeFilters]; + this.state.filterValues = {...FILTER_DEFAULTS, ...normalized.filterValues}; + } + + get currentFilterConfig() { + return this.normalizeFilterConfig({ + activeFilters: this.state.activeFilters, + filterValues: this.state.filterValues, + }); + } + + get isCurrentFilterDefault() { + if (!this.state.savedDefault) { + return false; + } + return ( + JSON.stringify(this.currentFilterConfig) === + JSON.stringify(this.normalizeFilterConfig(this.state.savedDefault)) + ); + } + + /** Whether the ★ has anything to act on: an active filter or a saved one. */ + get canStarFilter() { + return this.state.activeFilters.length > 0 || Boolean(this.state.savedDefault); + } + + get starLabel() { + return this.isCurrentFilterDefault ? _t("Default") : _t("Set default"); + } + + get starTitle() { + return this.isCurrentFilterDefault + ? _t("Default filter for %(operation)s — tap to remove.", { + operation: this.operationLabel, + }) + : _t("Save this filter as the default for %(operation)s.", { + operation: this.operationLabel, + }); + } + + async persistDefaultFilters() { + await this.inventory.write("res.users", [user.userId], { + barcode_default_filters: this.allDefaults, + }); + } + + /** + * Star: save the current filter as this operation's default. Filled star: + * remove it. Starring an empty filter is the same as clearing the default. + */ + async toggleDefaultFilter() { + const type = this.operationType; + if (!type) { + return; + } + const config = this.currentFilterConfig; + const clearing = this.isCurrentFilterDefault || !config.activeFilters.length; + if (clearing) { + if (!this.state.savedDefault) { + this.inventory.notify( + _t("Add a filter first, then save it as the default."), + {type: "warning"} + ); + return; + } + delete this.allDefaults[type]; + this.state.savedDefault = null; + await this.persistDefaultFilters(); + this.inventory.notify( + _t("Default filter cleared for %(operation)s.", { + operation: this.operationLabel, + }), + {type: "info"} + ); + return; + } + this.allDefaults[type] = config; + this.state.savedDefault = config; + await this.persistDefaultFilters(); + this.inventory.notify( + _t("Filter saved as the default for %(operation)s.", { + operation: this.operationLabel, + }), + {type: "success"} + ); + } + + countGroupEntries(groupNode) { + if (Array.isArray(groupNode)) { + return groupNode.length; + } + return Object.values(groupNode).reduce( + (count, childGroup) => count + this.countGroupEntries(childGroup), + 0 + ); + } + + sortedGroupKeys(groups) { + return Object.keys(groups).sort((left, right) => + left.localeCompare(right, undefined, { + numeric: true, + sensitivity: "base", + }) + ); + } + + getGroupLevelLabel(level) { + return this.groupLabels[this.state.groupByLevels[level]] || _t("Group"); + } + + getGroupSummary(groupNode) { + const count = this.countGroupEntries(groupNode); + return `${count} ${count === 1 ? _t("picking") : _t("pickings")}`; + } +} +PickingListScreen.template = "barcode_scanner.PickingListScreen"; + +barcodeScreens.add("picking_list", {component: PickingListScreen}); diff --git a/barcode_stock/static/src/js/screens/picking_screen.esm.js b/barcode_stock/static/src/js/screens/picking_screen.esm.js new file mode 100644 index 0000000..736ac0b --- /dev/null +++ b/barcode_stock/static/src/js/screens/picking_screen.esm.js @@ -0,0 +1,752 @@ +import {Component, onWillStart, onWillUpdateProps, useState} from "@odoo/owl"; +import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog"; +import {PickingDoneList} from "@barcode_stock/js/components/picking_done_list.esm"; +import {PickingInfoTab} from "@barcode_stock/js/components/picking_info_tab.esm"; +import {PickingMoveList} from "@barcode_stock/js/components/picking_move_list.esm"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeMatchDomain} from "@barcode_scanner/js/utils/scan_match.esm"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {tabForMove} from "@barcode_stock/js/utils/move_progress.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class PickingScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.action = useService("action"); + this.dialog = useService("dialog"); + this.feedback = useService("barcodeScannerFeedback"); + this.barcodeScannerState = useService("barcodeScannerState"); + this.barcodeScannerSync = useService("barcodeScannerSync"); + this.setTab = this.setTab.bind(this); + this.deleteMoveLine = this.deleteMoveLine.bind(this); + this.openMoveWizard = this.openMoveWizard.bind(this); + this.onBarcodeScanned = this.onBarcodeScanned.bind(this); + useBarcodeHandler({ + onScan: (barcode, parsedData, payload) => { + this.onBarcodeScanned(barcode, parsedData, payload); + }, + }); + + this.selectResponsible = this.selectResponsible.bind(this); + + this.state = useState({ + picking: null, + moves: [], + moveLines: [], + activeTab: "todo", + isValidating: false, + isSaving: false, + pickingTypeCode: null, + highlightedMoveId: null, + lastScan: { + barcode: "", + source: "", + tone: "idle", + message: _t("Scanner ready for this picking."), + productName: "", + lotName: "", + quantity: 0, + timestamp: null, + }, + }); + + onWillStart(async () => { + try { + const responsible = this.props.params?.responsible; + if (responsible && responsible.id && this.pickingId) { + await this.inventory.write("stock.picking", [this.pickingId], { + user_id: responsible.id, + }); + } + // A move confirmed in the wizard comes back as the move + // to focus, so the reload below lands on its tab. + this.state.highlightedMoveId = this.focusMoveId; + await this.loadData({force: true}); + if (responsible && responsible.id) { + this.state.activeTab = "info"; + this.inventory.notify( + _t("Responsible set to %(name)s", {name: responsible.name}), + { + type: "success", + } + ); + } + } catch (error) { + this.inventory.notify(_t("Error loading picking: %(error)s", {error}), { + type: "danger", + }); + } + }); + + onWillUpdateProps(async (nextProps) => { + const currentId = this.props.pickingId || this.props.params?.pickingId; + const nextId = nextProps.pickingId || nextProps.params?.pickingId; + const currentReloadToken = + this.props.reloadToken || this.props.params?.reloadToken; + const nextReloadToken = + nextProps.reloadToken || nextProps.params?.reloadToken; + + if (currentId !== nextId || currentReloadToken !== nextReloadToken) { + this.state.highlightedMoveId = + nextProps.focusMoveId || nextProps.params?.focusMoveId || null; + await this.loadData({force: true}); + } + }); + } + + get pickingId() { + return this.props.pickingId || this.props.params?.pickingId; + } + + get focusMoveId() { + return this.props.focusMoveId || this.props.params?.focusMoveId || null; + } + + get listParams() { + return this.props.listParams || this.props.params?.listParams || null; + } + + get operationLabel() { + const labels = { + incoming: _t("Receipt"), + internal: _t("Internal Transfer"), + outgoing: _t("Delivery Order"), + }; + return labels[this.state.pickingTypeCode] || _t("Picking"); + } + + get doneLines() { + return this.state.moveLines.filter((line) => (line.qty_picked || 0) > 0); + } + + get moveStatsById() { + const moveLineTotals = this.state.moveLines.reduce((totals, line) => { + const moveId = line.move_id?.[0]; + if (!moveId) { + return totals; + } + totals[moveId] = (totals[moveId] || 0) + (line.qty_picked || 0); + return totals; + }, {}); + + return Object.fromEntries( + this.state.moves.map((move) => { + const demandQty = move.product_uom_qty || move.quantity || 0; + const reservedQty = move.quantity || 0; + const doneQty = moveLineTotals[move.id] || 0; + const remainingQty = Math.max(reservedQty - doneQty, 0); + const completion = demandQty ? Math.min(doneQty / demandQty, 1) : 0; + return [ + move.id, + { + demandQty, + doneQty, + remainingQty, + completion, + completionLabel: `${Math.round(completion * 100)}%`, + }, + ]; + }) + ); + } + + get pendingMovesCount() { + return this.state.moves.filter( + (move) => (this.moveStatsById[move.id]?.remainingQty || 0) > 0 + ).length; + } + + get completedMovesCount() { + return this.state.moves.filter( + (move) => (this.moveStatsById[move.id]?.remainingQty || 0) <= 0 + ).length; + } + + get progressSummary() { + const summary = Object.values(this.moveStatsById).reduce( + (acc, stats) => { + acc.demandQty += stats.demandQty || 0; + acc.doneQty += stats.doneQty || 0; + acc.remainingQty += stats.remainingQty || 0; + return acc; + }, + {demandQty: 0, doneQty: 0, remainingQty: 0} + ); + summary.percent = summary.demandQty + ? Math.round((summary.doneQty / summary.demandQty) * 100) + : 0; + return summary; + } + + get summaryCards() { + return [ + { + key: "pending", + label: _t("Pending moves"), + value: this.pendingMovesCount, + tone: "warning", + }, + { + key: "done", + label: _t("Done lines"), + value: this.doneLines.length, + tone: "success", + }, + { + key: "progress", + label: _t("Progress"), + value: `${this.progressSummary.percent}%`, + tone: "info", + }, + ]; + } + + setLastScanContext({ + barcode = "", + source = "hardware", + tone = "info", + message = "", + move = null, + quantity = 0, + lotName = "", + } = {}) { + this.state.lastScan = { + barcode, + source, + tone, + message, + productName: move?.product_id?.[1] || "", + lotName, + quantity, + timestamp: Date.now(), + }; + this.state.highlightedMoveId = move?.id || null; + this.focusScannedMove(); + } + + /** + * Show the tab that actually holds the highlighted move. A move still + * pending lives in "To Do"; once it is fully picked only "Done" lists it, + * so switching to "To Do" after every scan left the operator in front of a + * list without the line the notification had just confirmed. + */ + focusScannedMove() { + const moveId = this.state.highlightedMoveId; + if (!moveId) { + return; + } + const move = this.state.moves.find((candidate) => candidate.id === moveId); + if (!move) { + return; + } + this.state.activeTab = tabForMove(move); + } + + async loadData({force = false} = {}) { + const pickingId = this.pickingId; + if ( + force || + !( + this.barcodeScannerState.ready && + this.barcodeScannerState.activePickingId === pickingId + ) + ) { + await this.barcodeScannerState.preloadPicking(pickingId); + } + const snapshot = this.barcodeScannerState.getSnapshot(); + const picking = snapshot.picking || {}; + this.state.pickingTypeCode = snapshot.pickingTypeCode; + this.state.moves = await this.buildMovesWithLots(snapshot.moves || []); + this.state.picking = {...picking}; + this.state.moveLines = [...(snapshot.moveLines || [])]; + if (picking.scheduled_date) { + this.state.picking.scheduled_date = picking.scheduled_date + .replace(" ", "T") + .slice(0, 16); + } + // The scanned line may have just become complete, which moves it from + // one tab to the other; decide again on the data that was just read. + this.focusScannedMove(); + } + + setTab(tab) { + this.state.activeTab = tab; + } + + goBack() { + this.store.goBack(); + } + + selectResponsible() { + this.store.navigate("user_selector", { + returnRoute: "picking", + returnParams: { + pickingId: this.pickingId, + listParams: this.listParams, + }, + }); + } + + async savePicking() { + if (!this.state.picking?.id) return; + this.state.isSaving = true; + try { + await this.barcodeScannerSync.stagePicking( + { + scheduled_date: this.state.picking.scheduled_date || false, + note: this.state.picking.note || false, + }, + {immediate: true} + ); + this.inventory.notify(_t("Changes saved successfully"), { + type: "success", + }); + } catch { + this.inventory.notify(_t("Error while saving"), { + type: "danger", + }); + } finally { + this.state.isSaving = false; + } + } + + async validatePicking() { + if (!this.state.picking?.id) return; + + if (!this.checkAnyMoveLineWithQty()) { + this.inventory.notify(_t("Please add at least one product to validate."), { + type: "danger", + }); + return; + } + + const guardrailErrors = this.barcodeScannerSync.validatePickingForConfirm(); + if (guardrailErrors.length > 0) { + for (const err of guardrailErrors) { + this.inventory.notify(err.message, {type: "danger"}); + } + return; + } + + this.state.isValidating = true; + try { + await this.savePicking(); + const action = await this.inventory.call( + "stock.picking", + "button_validate", + [this.state.picking.id], + {context: {active_ids: [this.state.picking.id]}} + ); + if ( + action && + action.type === "ir.actions.act_window" && + action.res_model === "stock.backorder.confirmation" + ) { + await this.action.doAction(action, { + onClose: () => { + this.store.navigate("picking_list", this.listParams || {}, { + clearHistory: true, + }); + }, + }); + } else if (action && action.type === "ir.actions.act_window") { + await this.action.doAction(action); + this.store.navigate("picking_list", this.listParams || {}, { + clearHistory: true, + }); + } else { + this.store.navigate("picking_list", this.listParams || {}, { + clearHistory: true, + }); + } + } catch (error) { + this.inventory.notify( + _t("Validation failed: %(error)s", {error: error.message}), + { + type: "danger", + } + ); + } finally { + this.state.isValidating = false; + } + } + + async _reloadMoves() { + await this.loadData({force: true}); + } + + async cancelReservation() { + this.dialog.add(ConfirmationDialog, { + title: _t("Confirm cancel reservations"), + body: _t("Are you sure you want to cancel reservations?"), + confirm: async () => { + try { + this.state.highlightedMoveId = null; + await this.inventory.write( + "stock.move.line", + this.state.moveLines.map((ml) => ml.id), + {qty_picked: 0} + ); + await this.inventory.call("stock.picking", "do_unreserve", [ + [this.pickingId], + ]); + this.inventory.notify(_t("Cancel Reservation successfully"), { + type: "success", + }); + } catch (error) { + this.inventory.notify( + _t("Cancel Reservation error: %(error)s", {error}), + { + type: "error", + } + ); + } + await this._reloadMoves(); + }, + }); + } + + async checkAvailability() { + this.dialog.add(ConfirmationDialog, { + title: _t("Confirm availability check"), + body: _t("Are you sure you want to check availability?"), + confirm: async () => { + try { + await this.inventory.call("stock.picking", "action_assign", [ + [this.pickingId], + ]); + this.inventory.notify(_t("Availability check successful."), { + type: "success", + }); + await this._reloadMoves(); + } catch (error) { + this.inventory.notify( + _t("Availability check error: %(error)s", {error}), + { + type: "error", + } + ); + } + }, + }); + } + + openMoveWizard(move, defaults = {}) { + if (move.quantity === 0) { + this.inventory.notify(_t("This move has no quantity to process."), { + type: "warning", + }); + return; + } + this.store.navigate("move_wizard", { + moveId: move.id, + pickingId: this.state.picking.id, + pickingTypeCode: this.state.pickingTypeCode, + defaultQty: defaults.qty || 1, + defaultLot: defaults.lot || null, + lotName: defaults.lotName || null, + expiration: defaults.expiration || null, + createLot: defaults.createLot || false, + listParams: this.listParams, + }); + } + + async deleteMoveLine(moveLineId) { + this.dialog.add(ConfirmationDialog, { + title: _t("Confirm deletion"), + body: _t("Are you sure you want to delete this movement?"), + confirm: async () => { + // Undoing a confirmation voids the scan that produced it, so + // the reload below must not chase the line to another tab. + this.state.highlightedMoveId = null; + await this.barcodeScannerSync.resetMoveLine(moveLineId, { + immediate: true, + }); + this.state.moveLines = this.state.moveLines.filter( + (l) => l.id !== moveLineId + ); + await this._reloadMoves(); + }, + }); + } + + async createMoveLine(moveId, productId, qty, lotId = null) { + const move = this.barcodeScannerState.getMove(moveId); + const lot = lotId ? this.barcodeScannerState.lotsById[lotId] : null; + await this.barcodeScannerSync.confirmMove({ + moveId, + pickingId: this.state.picking.id, + productId, + qtyPicked: qty, + lotId: lotId || false, + lotName: lot?.name || false, + locationId: move?.location_id?.[0] || false, + locationDestId: move?.location_dest_id?.[0] || false, + }); + + await this._reloadMoves(); + + this.inventory.notify(_t("Product updated"), { + type: "success", + }); + } + + async openMoveForProduct(barcode, preselectedMove = null, {qty = 1} = {}) { + if (preselectedMove) { + this.setLastScanContext({ + barcode, + tone: "success", + message: _t("Product matched and ready to confirm."), + move: preselectedMove, + quantity: qty, + }); + this.openMoveWizard(preselectedMove, { + qty, + }); + return; + } + const productDomain = barcodeMatchDomain(barcode); + const products = productDomain + ? await this.inventory.searchRead("product.product", productDomain, [ + "display_name", + "tracking", + ]) + : []; + if (!products.length) { + this.setLastScanContext({ + barcode, + tone: "warning", + message: _t("Scanned product was not found."), + }); + this.inventory.notify(_t("Product not found"), {type: "danger"}); + return; + } + const product = products[0]; + const move = this.state.moves.find((m) => m.product_id?.[0] === product.id); + if (!move) { + this.setLastScanContext({ + barcode, + tone: "warning", + message: _t("This product is not reserved for this picking."), + }); + this.inventory.notify(_t("Product not reserved"), { + type: "warning", + }); + return; + } + this.setLastScanContext({ + barcode, + tone: "success", + message: _t("Product matched and ready to confirm."), + move, + quantity: qty, + }); + this.openMoveWizard(move, { + qty, + }); + } + + /** + * A scan carrying a lot or serial number opens the wizard with it already + * resolved: a known lot is preselected, and a lot the operation cannot pick + * from stock goes through the create-lot flow — available on receipts only, + * the one operation allowed to register lots. + */ + async handleScannedLot(move, normalized, source) { + const {barcode, lot, quantity, expiration} = normalized; + const lotName = lot?.name || normalized.lotName; + const isOutgoing = this.state.pickingTypeCode === "outgoing"; + const today = new Date().toISOString().slice(0, 10); + const expired = + (lot && this.barcodeScannerState.isLotExpired(lot.id)) || + (expiration && expiration < today); + if (expired) { + const message = _t("Lot %(lot)s has passed its expiration date.", { + lot: lotName, + }); + this.feedback.warning({notify: true, message}); + // A reception records what physically arrived and an internal move + // just relocates the stock, so both warn and let the scan proceed; + // only a delivery to a customer stays a hard stop. + if (isOutgoing) { + this.setLastScanContext({ + barcode, + source, + tone: "warning", + message, + move, + lotName, + }); + return; + } + } + if (lot && this.barcodeScannerState.useExistingLots) { + this.setLastScanContext({ + barcode, + source, + tone: "success", + message: _t("Lot %(lot)s matched and ready to confirm.", { + lot: lot.name, + }), + move, + quantity, + lotName: lot.name, + }); + this.openMoveWizard(move, { + qty: quantity, + lot: lot.id, + lotName: lot.name, + }); + return; + } + + const canCreateLot = + this.state.pickingTypeCode === "incoming" && + this.barcodeScannerState.useCreateLots; + if (!canCreateLot) { + const message = _t("Lot %(lot)s is not available for this product.", { + lot: lotName, + }); + this.setLastScanContext({ + barcode, + source, + tone: "warning", + message, + move, + lotName, + }); + this.feedback.warning({notify: true, message}); + return; + } + this.setLastScanContext({ + barcode, + source, + tone: "success", + message: _t("Lot %(lot)s ready to be registered.", {lot: lotName}), + move, + quantity, + lotName, + }); + this.openMoveWizard(move, { + qty: quantity, + lotName, + expiration, + createLot: true, + }); + } + + onBarcodeScanned(barcode, parsedData = null, payload = {}) { + this.handleBarcode(barcode, parsedData, payload); + } + + async handleBarcode(barcode, parsedData = null, payload = {}) { + const normalized = this.barcodeScannerState.applyScanResult({ + barcode, + ...(parsedData || {}), + }); + const candidateMove = normalized.candidates[0] || null; + const source = payload?.source || "hardware"; + if (!normalized.candidates.length) { + this.setLastScanContext({ + barcode, + source, + tone: "warning", + message: _t("Scanned barcode is not part of the current picking."), + }); + this.feedback.warning({ + notify: true, + message: _t("Scanned barcode is not part of the current picking."), + }); + return; + } + + const productId = candidateMove.product_id?.[0]; + const tracking = + this.barcodeScannerState.indexes.trackingByProductId[productId]; + // The scan states how much it represents (one unit for a plain product + // barcode, a count or a net weight for a barcode that carries one). + const scannedQty = normalized.quantity; + if (tracking === "none") { + const existingLine = this.state.moveLines.find( + (line) => + line.move_id?.[0] === candidateMove.id && + line.product_id?.[0] === productId && + (line.qty_picked || 0) > 0 + ); + if (existingLine) { + // NewQty is the resulting total, only used for the feedback + // message below. confirmMove()/stageMoveLine() already increment + // the existing line by the qtyPicked it receives, so pass the + // scanned delta here -- passing the total would add the existing + // quantity a second time (a re-scan jumped by +2 instead of +1). + const newQty = (existingLine.qty_picked || 0) + scannedQty; + await this.barcodeScannerSync.confirmMove({ + moveId: candidateMove.id, + pickingId: this.state.picking.id, + productId, + qtyPicked: scannedQty, + lotId: false, + lotName: false, + locationId: candidateMove.location_id?.[0] || false, + locationDestId: candidateMove.location_dest_id?.[0] || false, + }); + this.setLastScanContext({ + barcode, + source, + tone: "success", + message: _t("Quantity incremented to %(qty)s.", {qty: newQty}), + move: candidateMove, + quantity: newQty, + }); + this.feedback.success({ + notify: true, + message: _t("Quantity incremented to %(qty)s.", {qty: newQty}), + }); + await this._reloadMoves(); + return; + } + } else if (normalized.lotName) { + await this.handleScannedLot(candidateMove, normalized, source); + return; + } + + await this.openMoveForProduct(barcode, candidateMove, {qty: scannedQty}); + if (payload?.source === "camera" && candidateMove) { + this.feedback.success(); + } + } + + async buildMovesWithLots(moves) { + const lotsMap = Object.fromEntries( + Object.values(this.barcodeScannerState.lotsById).map((lot) => [ + lot.id, + lot.name, + ]) + ); + return moves.map((move) => ({ + ...move, + lot_names: (move.lot_ids || []).map((id) => lotsMap[id]), + })); + } + + checkAnyMoveLineWithQty() { + return this.state.moveLines.some((line) => line.qty_picked > 0); + } +} + +PickingScreen.template = "barcode_scanner.PickingScreen"; +PickingScreen.components = {PickingInfoTab, PickingMoveList, PickingDoneList}; + +barcodeScreens.add("picking", { + component: PickingScreen, + props: (params) => ({ + pickingId: params.pickingId, + listParams: params.listParams || null, + reloadToken: params.reloadToken || null, + focusMoveId: params.focusMoveId || null, + params, + }), +}); diff --git a/barcode_stock/static/src/js/screens/product_selector_screen.esm.js b/barcode_stock/static/src/js/screens/product_selector_screen.esm.js new file mode 100644 index 0000000..11389b7 --- /dev/null +++ b/barcode_stock/static/src/js/screens/product_selector_screen.esm.js @@ -0,0 +1,181 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeMatchDomain} from "@barcode_scanner/js/utils/scan_match.esm"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {recordImageUrl} from "@barcode_stock/js/utils/avatar.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class ProductSelectorScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.state = useState({ + products: [], + search: "", + loading: true, + selectedProduct: null, + qty: 1, + }); + + useBarcodeHandler({ + onScan: async (barcode, parsedData) => { + await this.onBarcodeScanned(barcode, parsedData); + }, + }); + + onWillStart(async () => { + await this.loadProducts(); + }); + } + + async onBarcodeScanned(barcode, parsedData) { + const searchCode = parsedData?.value || barcode; + const domain = barcodeMatchDomain(searchCode); + // No image here: the scanned product goes straight to confirmSelection, + // nothing renders it, and it would cost a full base64 image. + const products = domain + ? await this.inventory.searchRead("product.product", domain, [ + "name", + "standard_price", + "tracking", + "default_code", + "type", + ]) + : []; + if (products.length) { + this.state.selectedProduct = products[0]; + this.confirmSelection(); + return; + } + // No product carries this barcode: filter the list by it and say so, + // instead of silently doing nothing. + this.state.search = searchCode; + this.inventory.notify( + _t("No product matches “%(code)s”.", {code: searchCode}), + {type: "warning"} + ); + } + + /** + * Should the image fail anyway -- no session, no network, a filestore that + * lost the file -- fall back to the initial rather than leave a broken + * image in the list. + */ + onImageError(record) { + record.image_url = false; + } + + async loadProducts() { + const domain = [["type", "=", "consu"]]; + // `bin_size` keeps the images themselves off the wire -- see + // recordImageUrl. + let products = await this.inventory.searchRead( + "product.product", + domain, + [ + "name", + "image_128", + "standard_price", + "tracking", + "default_code", + "type", + "write_date", + ], + {context: {bin_size: true}} + ); + + products = products.map((prod) => { + prod.image_url = recordImageUrl("product.product", prod); + return prod; + }); + + this.state.products = products; + this.state.loading = false; + } + + get filteredProducts() { + if (!this.state.search) { + return this.state.products; + } + const search = this.state.search.toLowerCase(); + return this.state.products.filter( + (p) => + p.name.toLowerCase().includes(search) || + (p.default_code && p.default_code.toLowerCase().includes(search)) + ); + } + + selectProduct(product) { + this.state.selectedProduct = product; + } + + incrementQty() { + this.state.qty = (parseInt(this.state.qty, 10) || 0) + 1; + } + + decrementQty() { + const current = parseInt(this.state.qty, 10) || 1; + if (current > 1) { + this.state.qty = current - 1; + } + } + + onQtyInput(ev) { + const val = parseInt(ev.target.value, 10); + this.state.qty = isNaN(val) || val < 1 ? 1 : val; + } + + async confirmSelection() { + const product = this.state.selectedProduct; + if (!product) return; + + if (this.props.params.mode === "quick_info_product") { + this.store.goBack({ + result: product, + result_type: "product", + }); + return; + } + + const qty = parseInt(this.state.qty, 10) || 1; + const lines = this.props.params.lines || []; + const existing = lines.find((l) => l.product_id === product.id); + const lots = await this.inventory.searchRead( + "stock.lot", + [["product_id", "=", product.id]], + ["id", "name"] + ); + if (existing) { + existing.qty += qty; + } else { + lines.push({ + product_id: product.id, + product_name: product.name, + qty: qty, + price_unit: product.standard_price || 0, + tracking: product.tracking || "none", + lot_id: null, + lots: lots, + default_code: product.default_code || "", + }); + } + this.store.goBack({ + ...this.props.params, + lines: lines, + }); + } + + updateSearch(ev) { + this.state.search = ev.target.value; + } + + goBack() { + this.store.goBack(); + } +} + +ProductSelectorScreen.template = "barcode_scanner.ProductSelectorScreen"; + +barcodeScreens.add("product_selector", {component: ProductSelectorScreen}); diff --git a/barcode_stock/static/src/js/screens/quick_info.esm.js b/barcode_stock/static/src/js/screens/quick_info.esm.js new file mode 100644 index 0000000..cd75725 --- /dev/null +++ b/barcode_stock/static/src/js/screens/quick_info.esm.js @@ -0,0 +1,281 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import { + barcodeMatchAnyDomain, + barcodeMatchDomain, +} from "@barcode_scanner/js/utils/scan_match.esm"; +import {_t} from "@web/core/l10n/translation"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {parseBarcode} from "@barcode_scanner/js/barcode_parser.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class QuickInfoScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.state = useState({ + barcode: "", + result: null, + resultType: null, + resultDetails: null, + locationStock: [], + productStock: [], + stockPage: 0, + lot: null, + lotStock: [], + }); + this.stockPageSize = 15; + + useBarcodeHandler({ + onScan: async (barcode, parsedData) => { + await this.onBarcodeScanned(barcode, parsedData); + }, + }); + + onWillStart(async () => { + if (this.props.params && this.props.params.mode) { + this.state.mode = this.props.params.mode; + } + if (this.props.params && this.props.params.result) { + await this.loadResult( + this.props.params.result, + this.props.params.result_type + ); + } + }); + } + + async loadResult(result, resultType, {lotName = null} = {}) { + this.state.result = result; + this.state.resultType = resultType; + this.state.locationStock = []; + this.state.productStock = []; + this.state.stockPage = 0; + this.state.lot = null; + this.state.lotStock = []; + if (resultType === "product") { + const products = await this.inventory.searchRead( + "product.product", + [["id", "=", result.id]], + [ + "name", + "default_code", + "barcode", + "standard_price", + "list_price", + "tracking", + "type", + "is_storable", + "qty_available", + "uom_id", + ] + ); + this.state.resultDetails = products.length ? products[0] : null; + if (this.state.resultDetails) { + // Where this product physically sits: on-hand grouped by + // internal location -- the mirror of the location->products + // direction, which was the missing half of the quick lookup. + const quants = await this.inventory.readGroup( + "stock.quant", + [ + ["product_id", "=", result.id], + ["location_id.usage", "=", "internal"], + ], + ["location_id", "quantity"], + ["location_id"] + ); + this.state.productStock = quants + .filter((q) => q.quantity > 0) + .map((q) => ({ + locationId: q.location_id[0], + locationName: q.location_id[1], + quantity: q.quantity, + })); + } + if (this.state.resultDetails && lotName) { + await this.loadLot(result.id, lotName); + } + } else if (resultType === "location") { + const locations = await this.inventory.searchRead( + "stock.location", + [["id", "=", result.id]], + ["display_name", "barcode", "usage"] + ); + this.state.resultDetails = locations.length ? locations[0] : null; + if (this.state.resultDetails) { + const quants = await this.inventory.readGroup( + "stock.quant", + [["location_id", "=", result.id]], + ["product_id", "quantity"], + ["product_id"] + ); + this.state.locationStock = quants + .filter((q) => q.quantity > 0) + .map((q) => ({ + productId: q.product_id[0], + productName: q.product_id[1], + quantity: q.quantity, + })); + } + } + } + + /** + * A scan can name a lot or serial on top of the product, so show where that + * lot actually sits — the question an operator scanning a labelled pallet is + * really asking. + */ + async loadLot(productId, lotName) { + const [lot] = await this.inventory.searchRead( + "stock.lot", + [ + ["product_id", "=", productId], + ["name", "=", lotName], + ], + ["name"] + ); + if (!lot) { + this.inventory.notify( + _t("Lot %(lot)s not found for this product.", {lot: lotName}), + { + type: "warning", + } + ); + return; + } + this.state.lot = lot; + const quants = await this.inventory.readGroup( + "stock.quant", + [["lot_id", "=", lot.id]], + ["location_id", "quantity"], + ["location_id"] + ); + this.state.lotStock = quants + .filter((quant) => quant.quantity > 0) + .map((quant) => ({ + locationId: quant.location_id[0], + locationName: quant.location_id[1], + quantity: quant.quantity, + })); + } + + goBack() { + if (this.state.resultDetails) { + this.state.result = null; + this.state.resultDetails = null; + this.state.resultType = null; + this.state.locationStock = []; + this.state.productStock = []; + this.state.stockPage = 0; + this.state.lot = null; + this.state.lotStock = []; + return; + } + this.store.goBack(); + } + + openProductSelector() { + this.store.navigate("product_selector", { + mode: "quick_info_product", + return_mode: this.state.mode, + }); + } + + openLocationSelector() { + this.store.navigate("location_selector", { + mode: "quick_info_location", + return_mode: this.state.mode, + }); + } + + get paginatedStock() { + const start = this.state.stockPage * this.stockPageSize; + return this.state.locationStock.slice(start, start + this.stockPageSize); + } + + get stockTotalPages() { + return Math.ceil(this.state.locationStock.length / this.stockPageSize); + } + + nextStockPage() { + if (this.state.stockPage < this.stockTotalPages - 1) { + this.state.stockPage++; + } + } + + prevStockPage() { + if (this.state.stockPage > 0) { + this.state.stockPage--; + } + } + + onInputKeydown(ev) { + if (ev.key === "Enter") { + this.searchBarcode(); + } + } + + async searchBarcode() { + const barcode = this.state.barcode; + if (!barcode) { + this.inventory.notify(_t("Enter a barcode."), {type: "warning"}); + return; + } + // A typed barcode goes through the same parsers as a scanned one, so a + // code carrying more than a product reads the same either way. + await this.lookupAndShow(barcode, parseBarcode(barcode)); + } + + async onBarcodeScanned(barcode, parsedData) { + if (!(parsedData?.value || barcode)) { + this.inventory.notify(_t("Barcode not recognized."), {type: "warning"}); + return; + } + this.state.barcode = ""; + await this.lookupAndShow(barcode, parsedData); + } + + async lookupAndShow(barcode, parsedData = null) { + const lotName = parsedData?.lot || parsedData?.serial || null; + try { + // A GS1 GTIN has several equivalent forms; match every candidate so + // the product is found whichever variant it is stored under. + const candidates = [ + ...(parsedData?.productCodes || []), + parsedData?.value, + barcode, + ]; + const productDomain = barcodeMatchAnyDomain(candidates); + const products = productDomain + ? await this.inventory.searchRead("product.product", productDomain, [ + "display_name", + ]) + : []; + if (products.length) { + this.state.barcode = ""; + await this.loadResult(products[0], "product", {lotName}); + return; + } + const locationDomain = barcodeMatchDomain(barcode); + const locations = locationDomain + ? await this.inventory.searchRead("stock.location", locationDomain, [ + "display_name", + ]) + : []; + if (locations.length) { + this.state.barcode = ""; + await this.loadResult(locations[0], "location"); + return; + } + this.inventory.notify(_t("Barcode not found."), {type: "danger"}); + } catch (error) { + console.error(error); + this.inventory.notify(_t("Search failed."), {type: "danger"}); + } + } +} + +QuickInfoScreen.template = "barcode_scanner.QuickInfoScreen"; + +barcodeScreens.add("quick_info", {component: QuickInfoScreen}); diff --git a/barcode_stock/static/src/js/screens/user_selector_screen.esm.js b/barcode_stock/static/src/js/screens/user_selector_screen.esm.js new file mode 100644 index 0000000..1b8925e --- /dev/null +++ b/barcode_stock/static/src/js/screens/user_selector_screen.esm.js @@ -0,0 +1,102 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {recordImageUrl} from "@barcode_stock/js/utils/avatar.esm"; +import {useBarcodeHandler} from "@barcode_scanner/js/hooks/use_barcode_handler.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +const DEFAULT_RETURN_ROUTE = "internal_transfer"; + +export class UserSelectorScreen extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.state = useState({ + users: [], + search: "", + loading: true, + selectedUser: this.props.params?.responsible || null, + }); + + useBarcodeHandler({ + onScan: (barcode) => { + this.state.search = barcode; + }, + }); + + onWillStart(async () => { + await this.loadUsers(); + }); + } + + get _returnRoute() { + return this.props.params?.returnRoute || DEFAULT_RETURN_ROUTE; + } + + get _returnParams() { + return {...(this.props.params?.returnParams || {})}; + } + + /** + * Should the image fail anyway -- no session, no network, a filestore that + * lost the file -- fall back to the initial rather than leave a broken + * image in the list. + */ + onImageError(record) { + record.image_url = false; + } + + async loadUsers() { + // `bin_size` keeps the images themselves off the wire -- see + // recordImageUrl. + let users = await this.inventory.searchRead( + "res.users", + [["share", "=", false]], + ["name", "login", "image_128", "write_date"], + {context: {bin_size: true}} + ); + + users = users.map((user) => { + user.image_url = recordImageUrl("res.users", user); + return user; + }); + + this.state.users = users; + this.state.loading = false; + } + + get filteredUsers() { + if (!this.state.search) { + return this.state.users; + } + const s = this.state.search.toLowerCase(); + return this.state.users.filter( + (user) => + user.name.toLowerCase().includes(s) || + (user.login && user.login.toLowerCase().includes(s)) + ); + } + + selectUser(user) { + this.state.selectedUser = user; + } + + confirmSelection() { + const user = this.state.selectedUser; + if (!user) { + return; + } + this.store.goBack({ + ...this._returnParams, + responsible: {id: user.id, name: user.name}, + }); + } + + goBack() { + this.store.goBack(); + } +} + +UserSelectorScreen.template = "barcode_scanner.UserSelectorScreen"; + +barcodeScreens.add("user_selector", {component: UserSelectorScreen}); diff --git a/barcode_stock/static/src/js/screens/warehouse_ops.esm.js b/barcode_stock/static/src/js/screens/warehouse_ops.esm.js new file mode 100644 index 0000000..ab1469b --- /dev/null +++ b/barcode_stock/static/src/js/screens/warehouse_ops.esm.js @@ -0,0 +1,180 @@ +import { + Component, + onMounted, + onPatched, + onWillStart, + useRef, + useState, +} from "@odoo/owl"; +import {barcodeScreens} from "@barcode_scanner/js/registries.esm"; +import {useBarcodeScanner} from "@barcode_scanner/js/hooks/use_inventory.esm"; +import {useService} from "@web/core/utils/hooks"; + +// Remember the operator's warehouse choice across in-app navigation. The +// module-level variable is the RELIABLE layer: navigation is a client-side +// (SPA) screen swap, so WarehouseOps remounts and re-runs loadData, but this +// variable does NOT reset -- and unlike web storage it can't be blocked by a +// private-mode webview (sessionStorage alone silently failed there). Web +// storage is only a best-effort bonus so the pick also survives a full reload. +const WAREHOUSE_STORAGE_KEY = "barcode.warehouse_ops.selected_id"; +let lastSelectedWarehouseId = null; + +function readStoredWarehouseId() { + if (lastSelectedWarehouseId) { + return lastSelectedWarehouseId; + } + try { + const value = sessionStorage.getItem(WAREHOUSE_STORAGE_KEY); + return value ? parseInt(value, 10) : null; + } catch { + return null; + } +} + +function storeWarehouseId(id) { + lastSelectedWarehouseId = id || null; + try { + if (id) { + sessionStorage.setItem(WAREHOUSE_STORAGE_KEY, String(id)); + } + } catch { + // Web storage blocked: the module-level variable above still carries + // the pick across in-app navigation for this session. + } +} + +export class WarehouseOps extends Component { + setup() { + this.inventory = useBarcodeScanner(); + this.store = useService("barcodeStore"); + this.company = useService("company"); + this.state = useState({ + warehouses: [], + selectedWarehouseId: null, + }); + + // A + + + + + + + + + +
+ + + + + +
+
+ + + + + +
+ +
+ Loading... +
+
+ +
+
+ +

+ +

+
+
+ +
+ + + +
+ +
+ + + + + + + + + +
+
+ +
+ + + + + + + +
+
+
+
+ + + +
+ +
+
+
+

+ : +

+
+
+ + Items + +
+
+ + +
+ +
+ +
+
+
+ + + +

+ +

+
+
+ + + + +
+
+ +
+ +
+
+ + + + +
+ +
+ + Urgent + + + + + + + +
+
+
+
+
+
+ + + + + + + + +
+
+
+ + + +
+
+ +

Picking Operations

+
+
+ +
+
+ + +
+
+ + +
+ + +
+
+
Quick filters
+ +
+
+
Status
+ + + + +
+
+
Date
+ + + +
+ +
+
+
+ +
+
+
Group by
+ + + +
+
+
+ +
+ + + : + + + + + + + + + + + + +
+
+
+ +
+ +
Loading queue...
+
+ + + + + + + + + +
+
No pending pickings
+
+ Adjust filters or scan a picking name to continue. +
+
+
+
+
+
+
+ + +
+
+ +

Select Responsible

+
+
+ +
+ + +

AVAILABLE USERS

+ +
+ +
Loading users...
+
+ + +
+
+
+ + + + +
+ +
+
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+
+
+ No users found. +
+
+
+
+ +
+ +
+
+
+ + +
+ +
Loading move...
+
+ +
+ +

+ +

+ +
+ +
+
+
+
+ TASK STATUS +

Move Operation

+
+ + +
+
+
+ TO DO +
+
+
+
+
+
+ Picked + + Required + +
+
+ + +
+ + +
+ +
+ + +
+ + +
+
+ +
+ + +
+
+
+
+ +
+ +
+ + + + +
+
+ + +
+ +
+
+ +
+
+ + + + Select source +
+
+ +
+
+
+
+ +
+
+