diff --git a/README.md b/README.md index 84ead47c42..6597fd2a6a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ addon | version | maintainers | summary [shopfloor_mobile_base](shopfloor_mobile_base/) | 16.0.1.2.1 | simahawk | Mobile frontend for WMS Shopfloor app [shopfloor_mobile_base_auth_api_key](shopfloor_mobile_base_auth_api_key/) | 16.0.1.0.0 | | Provides authentication via API key to Shopfloor base mobile app [shopfloor_product_dimension](shopfloor_product_dimension/) | 16.0.1.0.0 | rousseldenis | This module allow to enrich product available details about its dimension in shopfloor -[shopfloor_reception](shopfloor_reception/) | 16.0.1.11.2 | mmequignon JuMiSanAr | Reception scenario for shopfloor +[shopfloor_reception](shopfloor_reception/) | 16.0.1.11.4 | mmequignon JuMiSanAr | Reception scenario for shopfloor [shopfloor_reception_mobile](shopfloor_reception_mobile/) | 16.0.1.4.0 | JuMiSanAr | Scenario for receiving products [shopfloor_reception_packaging_dimension](shopfloor_reception_packaging_dimension/) | 16.0.1.1.0 | TDu | Collect Packaging Dimension from the Reception scenario [shopfloor_reception_packaging_dimension_mobile](shopfloor_reception_packaging_dimension_mobile/) | 16.0.1.1.0 | TDu | Frontend for the packaging dimension on reception scenario diff --git a/shopfloor/i18n/it.po b/shopfloor/i18n/it.po index 7585047258..54a495f527 100644 --- a/shopfloor/i18n/it.po +++ b/shopfloor/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-01-08 17:42+0000\n" +"PO-Revision-Date: 2026-06-22 19:46+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.10.4\n" +"X-Generator: Weblate 5.15.2\n" #. module: shopfloor #: model:ir.model.fields,help:shopfloor.field_shopfloor_menu__pick_pack_same_time @@ -284,12 +284,12 @@ msgstr "Consente creazione movimento" #. module: shopfloor #: model:ir.model.fields,field_description:shopfloor.field_shopfloor_menu__allow_quantity_exceeding_demand msgid "Allow Quantity Exceeding Demand" -msgstr "" +msgstr "Consentire quantità superiore alla richiesta" #. module: shopfloor #: model:ir.model.fields,field_description:shopfloor.field_shopfloor_menu__allow_quantity_exceeding_demand_is_possible msgid "Allow Quantity Exceeding Demand Is Possible" -msgstr "" +msgstr "È possibile consentire una quantità superiore alla domanda" #. module: shopfloor #: model:ir.model.fields,field_description:shopfloor.field_shopfloor_menu__allow_return_is_possible @@ -573,6 +573,8 @@ msgstr "Merce caricata in {0.name}" msgid "" "If True, prevents shopfloor error in case you process more qty than planned." msgstr "" +"Se vero, previene l'errore del reparto nel caso si lavori più quantità della " +"pianificata." #. module: shopfloor #: model:ir.model.fields,help:shopfloor.field_stock_move_line__result_package_id diff --git a/shopfloor_reception/README.rst b/shopfloor_reception/README.rst index 5098624377..3426f65e1e 100644 --- a/shopfloor_reception/README.rst +++ b/shopfloor_reception/README.rst @@ -11,7 +11,7 @@ Shopfloor Reception !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:699ec229276cf6bc2cbee82b6a7745f9a647aedf5fb0227002bdeae20ef65032 + !! source digest: sha256:bc12b448e98940e187bbbc914cc85749f7c27319d3c4ce7f3dfc0f2228554e56 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/shopfloor_reception/__manifest__.py b/shopfloor_reception/__manifest__.py index 85cb6ad013..10e6e61c72 100644 --- a/shopfloor_reception/__manifest__.py +++ b/shopfloor_reception/__manifest__.py @@ -1,7 +1,7 @@ { "name": "Shopfloor Reception", "summary": "Reception scenario for shopfloor", - "version": "16.0.1.11.2", + "version": "16.0.1.11.4", "development_status": "Beta", "category": "Inventory", "website": "https://github.com/OCA/wms", diff --git a/shopfloor_reception/i18n/it.po b/shopfloor_reception/i18n/it.po index b5b0247bcf..fc897fd145 100644 --- a/shopfloor_reception/i18n/it.po +++ b/shopfloor_reception/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-06-03 07:05+0000\n" +"PO-Revision-Date: 2026-06-22 19:46+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -82,7 +82,7 @@ msgstr "" #: code:addons/shopfloor_reception/actions/message.py:0 #, python-format msgid "The scanned lot does not match the selected product" -msgstr "" +msgstr "Il lotto letto non corrisponde al prodotto selezionato" #. module: shopfloor_reception #. odoo-python diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index 14a045ee89..127d5c2afb 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -1223,7 +1223,11 @@ def scan_lot(self, picking_id, selected_line_id, barcode): if result.type == "lot": lot_name = result.value elif result.type == "expiration_date": - lot_expiration_date = result.value + # We need to ensure we have a `datetime` object (and not a + # `date` one) for valid comparison with stock.lot.expiration_date + lot_expiration_date = datetime.combine( + result.value, datetime.min.time() + ) elif ( result.type == "product" and result.raw != selected_line.product_id.barcode @@ -1285,8 +1289,11 @@ def set_lot_confirm_action( ) product = selected_line.product_id - lot = self.search_result.record or self._actions_for("search").lot_from_scan( - lot_name, product + search_result_record = self.search_result.record + lot = ( + search_result_record + if search_result_record and search_result_record._name == "stock.lot" + else self._actions_for("search").lot_from_scan(lot_name, product) ) if product.use_expiration_date and ( diff --git a/shopfloor_reception/static/description/index.html b/shopfloor_reception/static/description/index.html index 23668ea73a..fe8f93ba03 100644 --- a/shopfloor_reception/static/description/index.html +++ b/shopfloor_reception/static/description/index.html @@ -372,7 +372,7 @@

Shopfloor Reception

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:699ec229276cf6bc2cbee82b6a7745f9a647aedf5fb0227002bdeae20ef65032 +!! source digest: sha256:bc12b448e98940e187bbbc914cc85749f7c27319d3c4ce7f3dfc0f2228554e56 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

Shopfloor implementation of the reception scenario. diff --git a/shopfloor_reception/tests/test_scan_lot.py b/shopfloor_reception/tests/test_scan_lot.py index d14b9e56f1..5db4f49c0c 100644 --- a/shopfloor_reception/tests/test_scan_lot.py +++ b/shopfloor_reception/tests/test_scan_lot.py @@ -1,12 +1,10 @@ # Copyright 2026 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timezone from unittest import mock -from odoo import fields - -from odoo.addons.shopfloor.actions.barcode_parser import BarcodeResult +from odoo.addons.shopfloor.actions.barcode_parser import BarcodeParser, BarcodeResult from odoo.addons.shopfloor.actions.search import SearchAction, SearchResult from .common import CommonCase @@ -21,51 +19,86 @@ class TestScanLotName(CommonCase): @classmethod def setUpClassBaseData(cls): res = super().setUpClassBaseData() - cls.product_a.tracking = "lot" + + cls.picking = cls._create_picking() + cls.lot = cls._create_lot() + cls.selected_move_line = cls.picking.move_line_ids.filtered( + lambda l: l.product_id == cls.product_a + ) + # ↓ Put a barcode compatible with GS1 + cls.product_a.write({"tracking": "lot", "barcode": "11111111111111"}) + cls.selected_move_line.shopfloor_user_id = cls.env.uid + return res + def _generate_gs1( + self, product_barcode: str, lot_expiration_date: date, lot_name: str + ) -> str: + gs1_barcode = ( + f"{GTIN_AI}{product_barcode}" + f"{EXPIRATION_DATE_AI}{lot_expiration_date.strftime('%y%m%d')}" + f"{LOT_AI}{lot_name}" + ) + return gs1_barcode + + def _generate_search_result( + self, + full_barcode: str, + expiration_date: date, + lot_name: str, + product_barcode: str, + _type="none", + record=None, + ) -> SearchResult: + return SearchResult( + record=record, + type=_type, + parse_result=[ + BarcodeResult(type="unknown", value=full_barcode, raw=full_barcode), + BarcodeResult( + type="expiration_date", + value=expiration_date, + raw=expiration_date.strftime("%y%m%d"), + ), + BarcodeResult(type="lot", value=lot_name, raw=lot_name), + BarcodeResult( + type="product", value=product_barcode, raw=product_barcode + ), + ], + ) + def test_scan_lot_extract_expiration_date_new_lot(self): """ Test that the expiration date can be extracted from barcode scan (case when the lot does not already exsit in db) """ - picking = self._create_picking() - lot = self._create_lot() - expiration_date = fields.Datetime.from_string("2022-07-02") - selected_move_line = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a - ) - selected_move_line.shopfloor_user_id = self.env.uid - - gs1_barcode = ( - f"{GTIN_AI}01234567890128{EXPIRATION_DATE_AI}220702{LOT_AI}{lot.name}" + expiration_date = date(2022, 7, 2) + gs1_barcode = self._generate_gs1( + self.product_a.barcode, expiration_date, self.lot.name ) with mock.patch.object(SearchAction, "find") as mock_find: - mock_find.return_value = SearchResult( - record=None, - type="None", - parse_result=[ - BarcodeResult(type="unknown", value=gs1_barcode), - BarcodeResult(type="expiration_date", value=expiration_date), - BarcodeResult(type="lot", value=lot.name), - ], + mock_find.return_value = self._generate_search_result( + gs1_barcode, expiration_date, self.lot.name, self.product_a.barcode ) res = self.service.dispatch( "scan_lot", params={ - "picking_id": picking.id, - "selected_line_id": selected_move_line.id, - "barcode": lot.name, + "picking_id": self.picking.id, + "selected_line_id": self.selected_move_line.id, + "barcode": self.lot.name, }, ) self.assertEqual( res["data"]["set_lot"]["selected_move_line"][0]["lot"]["expiration_date"], - expiration_date.isoformat(), + datetime( + expiration_date.year, expiration_date.month, expiration_date.day + ).isoformat(), ) self.assertEqual( - res["data"]["set_lot"]["selected_move_line"][0]["lot"]["name"], lot.name + res["data"]["set_lot"]["selected_move_line"][0]["lot"]["name"], + self.lot.name, ) def test_scan_lot_extract_expiration_date_existing_lot(self): @@ -75,58 +108,41 @@ def test_scan_lot_extract_expiration_date_existing_lot(self): Ensure there is a warning in case of mismatch between expiration date found in the barcode and the one on the existing lot. """ - picking = self._create_picking() - - expiration_date = fields.Datetime.from_string("2022-07-02") - lot = self._create_lot(expiration_date=expiration_date) - self.assertEqual(lot.expiration_date, expiration_date) - - selected_move_line = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a - ) - selected_move_line.shopfloor_user_id = self.env.uid - - gs1_barcode = ( - f"{GTIN_AI}01234567890128{EXPIRATION_DATE_AI}220704{LOT_AI}{lot.name}" + expiration_date = date(2022, 7, 2) + gs1_barcode = self._generate_gs1( + self.product_a.barcode, expiration_date, self.lot.name ) with mock.patch.object(SearchAction, "find") as mock_find: - mock_find.return_value = SearchResult( - record=lot, - type="lot", - parse_result=[ - BarcodeResult(type="unknown", value=gs1_barcode), - BarcodeResult( - type="expiration_date", - value=expiration_date + timedelta(days=2), - ), - BarcodeResult(type="lot", value=lot.name), - ], + mock_find.return_value = self._generate_search_result( + gs1_barcode, + expiration_date, + self.lot.name, + self.product_a.barcode, + _type="lot", + record=self.lot, ) res = self.service.dispatch( "scan_lot", params={ - "picking_id": picking.id, - "selected_line_id": selected_move_line.id, - "barcode": lot.name, + "picking_id": self.picking.id, + "selected_line_id": self.selected_move_line.id, + "barcode": self.lot.name, }, ) self.assertEqual( - datetime.fromisoformat( - res["data"]["set_lot"]["selected_move_line"][0]["lot"][ - "expiration_date" - ] - ), - expiration_date.replace(tzinfo=UTC), + res["data"]["set_lot"]["selected_move_line"][0]["lot"]["expiration_date"], + datetime( + expiration_date.year, expiration_date.month, expiration_date.day + ).isoformat(), ) self.assertEqual( res["data"]["set_lot"]["selected_move_line"][0]["lot"]["name"], - lot.name, + self.lot.name, ) - self.assertEqual( - res["message"], - self.msg_store.lot_already_exists_different_expiration_date(lot), + self.assertMessage( + res, self.msg_store.lot_already_exists_different_expiration_date(self.lot) ) def test_scan_lot_name_auto_set_lot_on_move_line(self): @@ -134,67 +150,93 @@ def test_scan_lot_name_auto_set_lot_on_move_line(self): If lot exists and lot_name is set on the move line, auto-fill the lot_id and skip "sel_lot" state. """ - picking = self._create_picking() - lot = self._create_lot() - selected_move_line = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a - ) - selected_move_line.lot_name = lot.name + self.selected_move_line.lot_name = self.lot.name res = self.service.dispatch( "scan_line", params={ - "picking_id": picking.id, - "barcode": lot.name, + "picking_id": self.picking.id, + "barcode": self.lot.name, }, ) self.assertEqual(res["next_state"], "set_quantity") - self.assertEqual(selected_move_line.lot_id, lot) + self.assertEqual(self.selected_move_line.lot_id, self.lot) def test_scan_lot_wrong_product(self): """ Test that the system detects that the scanned lot is for another product than currently selected one. """ - picking = self._create_picking() - lot = self._create_lot() - expiration_date = fields.Datetime.from_string("2022-07-02") - selected_move_line = picking.move_line_ids.filtered( - lambda l: l.product_id == self.product_a - ) - selected_move_line.shopfloor_user_id = self.env.uid - + expiration_date = date(2022, 7, 2) other_product_barcode = "01223334444555" - - gs1_barcode = f"{GTIN_AI}{other_product_barcode}" - gs1_barcode += f"{EXPIRATION_DATE_AI}220702{LOT_AI}{lot.name}" + gs1_barcode = self._generate_gs1( + other_product_barcode, expiration_date, self.lot.name + ) with mock.patch.object(SearchAction, "find") as mock_find: - mock_find.return_value = SearchResult( - record=None, - type="None", - parse_result=[ - BarcodeResult(type="unknown", value=gs1_barcode), - BarcodeResult(type="expiration_date", value=expiration_date), - BarcodeResult(type="lot", value=lot.name), - BarcodeResult(type="product", value=other_product_barcode), - ], + mock_find.return_value = self._generate_search_result( + gs1_barcode, expiration_date, self.lot.name, other_product_barcode ) res = self.service.dispatch( "scan_lot", params={ - "picking_id": picking.id, - "selected_line_id": selected_move_line.id, - "barcode": lot.name, + "picking_id": self.picking.id, + "selected_line_id": self.selected_move_line.id, + "barcode": self.lot.name, }, ) self.assert_response( res, "set_lot", self.msg_store.lot_product_mismatch(), + data={ + "picking": self.data.picking(self.picking), + "selected_move_line": self._data_for_move_lines( + self.selected_move_line + ), + }, + ) + + def test_set_lot_from_select_move(self): + picking = self._create_picking() + lot = self._create_lot() + lot.expiration_date = None + selected_move_line = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + # selected_move_line.lot_id = lot + with mock.patch.object(BarcodeParser, "parse") as mock_parse: + # Note: the order here is important, the first to match a record + # in DB will determine the `type` (and `record`) of the `SearchResult` + mock_parse.return_value = [ + # -> Put "product" in first to test if no error in case the + # SearchResult type is not "lot" but "product" + BarcodeResult( + type="product", + value=selected_move_line.product_id.barcode, + raw=selected_move_line.product_id.barcode, + ), + BarcodeResult(type="lot", value=lot.name, raw=lot.name), + BarcodeResult( + type="expiration_date", + value=date(2025, 4, 15), + raw="250415", + ), + ] + response = self.service.dispatch( + "scan_line", + params={ + "picking_id": picking.id, + "barcode": lot.name, + }, + ) + self.assert_response( + response, + next_state="set_quantity", data={ "picking": self.data.picking(picking), "selected_move_line": self._data_for_move_lines(selected_move_line), + "confirmation_required": None, }, )