From 36646a5883b9c223c3ef4a68679502b34789d2ef Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 3 Sep 2026 14:52:48 +0200 Subject: [PATCH 1/2] [IMP] shopfloor: add `force_detailed_scan` option for cluster picking Add the `force_detailed_scan` scenario option and menu setting to prevent automatic line completion when a location is scanned during cluster picking. When `force_detailed_scan` is enabled, scanning the location returns the user to the start line state instead of automatically advancing to the destination scan state. This forces the operator to explicitly scan the product, lot/serial, or package barcode to validate the move line, preventing inventory drift when physical stock at the location differs from system expectations. --- shopfloor/data/shopfloor_scenario_data.xml | 3 ++- .../migrations/16.0.2.26.0/post-migrate.py | 25 +++++++++++++++++++ shopfloor/models/shopfloor_menu.py | 21 ++++++++++++++++ shopfloor/services/cluster_picking.py | 7 ++++++ .../tests/test_cluster_picking_scan_line.py | 11 ++++++++ shopfloor/views/shopfloor_menu.xml | 6 +++++ 6 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 shopfloor/migrations/16.0.2.26.0/post-migrate.py diff --git a/shopfloor/data/shopfloor_scenario_data.xml b/shopfloor/data/shopfloor_scenario_data.xml index 5ac71784aaf..f1336febc35 100644 --- a/shopfloor/data/shopfloor_scenario_data.xml +++ b/shopfloor/data/shopfloor_scenario_data.xml @@ -36,7 +36,8 @@ "unload_package_at_destination": true, "multiple_move_single_pack": true, "no_prefill_qty": true, - "scan_location_or_pack_first": true + "scan_location_or_pack_first": true, + "force_detailed_scan": true } diff --git a/shopfloor/migrations/16.0.2.26.0/post-migrate.py b/shopfloor/migrations/16.0.2.26.0/post-migrate.py new file mode 100644 index 00000000000..61bc788edd1 --- /dev/null +++ b/shopfloor/migrations/16.0.2.26.0/post-migrate.py @@ -0,0 +1,25 @@ +import json +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + env = api.Environment(cr, SUPERUSER_ID, {}) + cluster_picking_scenario = env.ref("shopfloor.scenario_cluster_picking") + _update_scenario_options(cluster_picking_scenario) + + +def _update_scenario_options(scenario): + options = scenario.options + options["force_detailed_scan"] = True + options_edit = json.dumps(options or {}, indent=4, sort_keys=True) + scenario.write({"options_edit": options_edit}) + _logger.info( + "Option 'force_detailed_scan' added to scenario %s", + scenario.name, + ) diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 28b63ed35fa..85c92070eba 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -269,6 +269,20 @@ class ShopfloorMenu(models.Model): compute="_compute_require_destination_package_is_possible" ) + force_detailed_scan = fields.Boolean( + help="Force the operator to explicitly scan the product, lot/serial number, " + "or package barcode to confirm a move line instead of relying on location " + "or system deductions.\n\n" + "Without this, Shopfloor may automatically validate unique products, lots, or packages " + "as soon as the location is scanned. In practice, the physical inventory at " + "the location might differ from what the system expects " + "leading to silent stock drift and broken traceability.", + ) + + force_detailed_scan_is_possible = fields.Boolean( + compute="_compute_force_detailed_scan_is_possible" + ) + @api.onchange("unload_package_at_destination") def _onchange_unload_package_at_destination(self): # Uncheck pick_pack_same_time when unload_package_at_destination is set to True @@ -553,6 +567,13 @@ def _compute_allow_quantity_exceeding_demand_is_possible(self): menu.scenario_id.has_option("allow_quantity_exceeding_demand") ) + @api.depends("scenario_id") + def _compute_force_detailed_scan_is_possible(self): + for menu in self: + menu.force_detailed_scan_is_possible = menu.scenario_id.has_option( + "force_detailed_scan" + ) + @api.constrains( "move_line_search_sort_order", "move_line_search_sort_order_custom_code" ) diff --git a/shopfloor/services/cluster_picking.py b/shopfloor/services/cluster_picking.py index cb138e74818..3a32f84146b 100644 --- a/shopfloor/services/cluster_picking.py +++ b/shopfloor/services/cluster_picking.py @@ -690,6 +690,13 @@ def _scan_line_by_location(self, picking, move_line, location): ), sublocation=location, ) + + # Prevent auto select of the move line in case detailed scan is ON + if self.work.menu.force_detailed_scan: + return self._response_for_start_line( + move_line, + sublocation=location, + ) quantity = self._get_prefill_qty(move_line) return self._response_for_scan_destination(move_line, qty_done=quantity) diff --git a/shopfloor/tests/test_cluster_picking_scan_line.py b/shopfloor/tests/test_cluster_picking_scan_line.py index 151c2a10ff3..98b5353312b 100644 --- a/shopfloor/tests/test_cluster_picking_scan_line.py +++ b/shopfloor/tests/test_cluster_picking_scan_line.py @@ -406,3 +406,14 @@ def test_scan_line_error_not_found(self): "NO_EXISTING_BARCODE", {"message_type": "error", "body": "Barcode not found"}, ) + + def test_scan_line_prevent_location_scan(self): + self.menu.sudo().force_detailed_scan = True + self._simulate_batch_selected(self.batch, in_package=True) + line = self.batch.picking_ids.move_line_ids + self._scan_line_error( + line=line, + scanned=line.location_id.barcode, + message=None, + sublocation=line.location_id, + ) diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index a210b791ed5..b48ddcbcc90 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -165,6 +165,12 @@ When selecting a move line, force the user to first scan a package or a location and not a product or a lot. + + + + Date: Thu, 3 Sep 2026 15:42:35 +0200 Subject: [PATCH 2/2] [IMP] shopfloor_mobile: Cluster Picking - update location card color state on sublocation scan --- .../wms/src/components/batch_picking_line_detail.esm.js | 6 +++++- .../static/wms/src/scenario/cluster_picking.esm.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.esm.js b/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.esm.js index 41b678464a4..794856fded8 100644 --- a/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.esm.js +++ b/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.esm.js @@ -8,6 +8,10 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", { props: { line: Object, + locationScanned: { + type: Boolean, + default: false, + }, articleScanned: { type: Boolean, default: false, @@ -38,7 +42,7 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", { :key="'batch-picking-line-detail-1'" :record="line" :options="{main: true, key_title: 'location_src.name', title_action_field: {action_val_path: 'location_src.barcode'}}" - :card_color="utils.colors.color_for(articleScanned ? 'screen_step_done': 'screen_step_todo')" + :card_color="utils.colors.color_for(locationScanned || articleScanned ? 'screen_step_done': 'screen_step_todo')" />