From 6ae37cde10f983df06219f932754020e6149bb66 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 26 Aug 2025 16:25:45 +0200 Subject: [PATCH 1/4] [IMP] shopfloor_reception: Don't rely on result package to select a line When users don't use packages for receptions, we should not rely only on its presence on lines to not select them. So, rely also on line progress completed at 100% (after split) to select another one with same criteria --- shopfloor_reception/services/reception.py | 9 +- shopfloor_reception/tests/test_select_move.py | 87 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index a79addf582f..49d146cc864 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -287,6 +287,8 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1): """ unassigned_lines = self.env["stock.move.line"] for line in move.move_line_ids: + if line.progress == 100.0: + continue if line.shopfloor_user_id.id == self.env.uid: return self._scan_line__recover(picking, line, qty_done) elif not line.shopfloor_user_id: @@ -544,8 +546,11 @@ def _scan_line__by_lot(self, picking, lot): """ lines = picking.move_line_ids.filtered( lambda l: ( - lot == l.lot_id - or (lot.name == l.lot_name and lot.product_id == l.product_id) + ( + lot == l.lot_id + or (lot.name == l.lot_name and lot.product_id == l.product_id) + ) + and not l.progress == 100.0 and not l.result_package_id ) ) diff --git a/shopfloor_reception/tests/test_select_move.py b/shopfloor_reception/tests/test_select_move.py index a184443e0a4..ede6fffae32 100644 --- a/shopfloor_reception/tests/test_select_move.py +++ b/shopfloor_reception/tests/test_select_move.py @@ -45,6 +45,93 @@ def test_scan_product(self): }, ) + def test_scan_product_partial(self): + # Scan a line + # Set a partial quantity done + # Try to scan the product again + # The selected line should be the other one + picking = self._create_picking() + lot = self._create_lot() + self.assertFalse(picking.printed) + selected_move_line = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + + # Activate INPUT location + selected_move_line.location_dest_id.sudo().active = True + + selected_move_line.lot_id = lot + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": lot.name}, + ) + data = self.data.picking(picking) + + self.assertTrue(selected_move_line.picking_id.printed) + self.assert_response( + response, + next_state="set_quantity", + data={ + "picking": data, + "selected_move_line": self.data.move_lines(selected_move_line), + "confirmation_required": None, + }, + ) + + selected_move_line.shopfloor_user_id = self.env.uid + response = self.service.dispatch( + "set_quantity", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": 5.0, + }, + ) + + response = self.service.dispatch( + "process_without_pack", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "quantity": 5.0, + }, + ) + data = self.data.picking(picking) + self.assert_response( + response, + next_state="set_destination", + data={ + "picking": data, + "selected_move_line": self.data.move_lines(selected_move_line), + }, + ) + + response = self.service.dispatch( + "set_destination", + params={ + "picking_id": picking.id, + "selected_line_id": selected_move_line.id, + "location_name": "INPUT", + }, + ) + self.assert_response( + response, + next_state="select_move", + data=self._data_for_select_move(picking), + ) + lines = picking.move_line_ids.filtered(lambda l: l.product_id == self.product_a) + self.assertEqual(2, len(lines)) + previous_line = selected_move_line + + response = self.service.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": lot.name}, + ) + + self.assertNotEqual( + previous_line.id, response["data"]["set_lot"]["selected_move_line"][0]["id"] + ) + def test_scan_packaging(self): picking = self._create_picking() self._add_package(picking) From ef1e2e98bf52e0076b6c2b7c7b1fe99b721a86aa Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 29 Jan 2026 12:42:39 +0100 Subject: [PATCH 2/4] [IMP] shopfloor_reception: use `shopfloor_unloaded` to determine if a line has been fully processed. --- shopfloor_reception/services/reception.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index 49d146cc864..7ae246d5c81 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -287,7 +287,7 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1): """ unassigned_lines = self.env["stock.move.line"] for line in move.move_line_ids: - if line.progress == 100.0: + if line.shopfloor_unloaded: continue if line.shopfloor_user_id.id == self.env.uid: return self._scan_line__recover(picking, line, qty_done) @@ -335,7 +335,7 @@ def _select_line__filter_lines_by_packaging__return(self, lines, packaging): return_line = fields.first( lines.filtered( lambda l: not l.package_id.product_packaging_id - and not l.result_package_id + and not l.shopfloor_unloaded and l.shopfloor_user_id.id in (False, self.env.uid) ) ) @@ -352,7 +352,7 @@ def _select_line__filter_lines_by_packaging(self, lines, packaging): return fields.first( lines.filtered( lambda l: l.package_id.product_packaging_id == packaging - and not l.result_package_id + and not l.shopfloor_unloaded and l.shopfloor_user_id.id in [False, self.env.uid] ) ) @@ -550,8 +550,7 @@ def _scan_line__by_lot(self, picking, lot): lot == l.lot_id or (lot.name == l.lot_name and lot.product_id == l.product_id) ) - and not l.progress == 100.0 - and not l.result_package_id + and not l.shopfloor_unloaded ) ) if not lines: @@ -573,7 +572,7 @@ def _scan_line__by_lot(self, picking, lot): def _scan_line__fallback(self, picking, barcode): # We might have lines with no lot, but with a lot_name. lines = picking.move_line_ids.filtered( - lambda l: l.lot_name == barcode and not l.result_package_id + lambda l: l.lot_name == barcode and not l.shopfloor_unloaded ) if not lines: return self._response_for_select_move( @@ -1385,7 +1384,11 @@ def process_without_pack(self, picking_id, selected_line_id, quantity): return self._response_for_set_destination(picking, selected_line) def _post_line(self, selected_line): + """ + Called when the product is unloaded at destination. + """ selected_line.reserved_uom_qty = selected_line.qty_done + selected_line.shopfloor_unloaded = True if ( selected_line.picking_id.is_shopfloor_created and self.work.menu.allow_return From 85b8ba5b3052f1a2ceb064ffa0c56e39f384f592 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 29 Jan 2026 12:45:42 +0100 Subject: [PATCH 3/4] [FIX] shopfloor_reception: prevent concurrent operation theft Add a test case to verify that when multiple users are working on the same lot, one user cannot take the move line from another when scanning a lot. --- shopfloor_reception/tests/test_select_move.py | 30 +++++++++++++++++++ .../tests/test_set_destination.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/shopfloor_reception/tests/test_select_move.py b/shopfloor_reception/tests/test_select_move.py index ede6fffae32..ad0ded79d9e 100644 --- a/shopfloor_reception/tests/test_select_move.py +++ b/shopfloor_reception/tests/test_select_move.py @@ -180,6 +180,36 @@ def test_scan_lot(self): }, ) + def test_scan_lot_concurrent(self): + """ + If 2 operators work on the same lot, the second operator + should not steal the move line of the first. + """ + picking = self._create_picking() + lot = self._create_lot() + + service_u1 = self.service + res_u1 = service_u1.dispatch( + "scan_line", + params={ + "picking_id": picking.id, + "barcode": lot.name, + }, + ) + # User 2 starts working on the same move + service_u2 = self._get_service_for_user(self.shopfloor_manager) + res_u2 = service_u2.dispatch( + "scan_line", + params={ + "picking_id": picking.id, + "barcode": lot.name, + }, + ) + self.assertNotEqual( + res_u1["data"]["set_lot"]["selected_move_line"][0]["id"], + res_u2["data"]["set_lot"]["selected_move_line"][0]["id"], + ) + def test_scan_not_tracked_product(self): self.product_a.tracking = "none" picking = self._create_picking() diff --git a/shopfloor_reception/tests/test_set_destination.py b/shopfloor_reception/tests/test_set_destination.py index 9f962c9fdb4..b5b3909aba5 100644 --- a/shopfloor_reception/tests/test_set_destination.py +++ b/shopfloor_reception/tests/test_set_destination.py @@ -225,7 +225,7 @@ def test_auto_posting_full_two_lines(self): # One move remaining in the picking, for product b, still to be processed self.assertEqual(picking.move_ids.product_id, self.product_b) - def test_auto_posting_concurent_work(self): + def test_auto_posting_concurrent_work(self): """Check 2 users working on the same move. With the auto post line option On. From 9e27606527c72b27c7511a850d89437ee88a876c Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 5 Feb 2026 09:29:41 +0100 Subject: [PATCH 4/4] [FIX] shopfloor_reception: restrict line selection to "current user" or "no user". A condition on the lines filtered lines has been added so to ensure that 2 users working on the same move do not steal move lines from one another. --- shopfloor_reception/services/reception.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index 7ae246d5c81..05d670d8b24 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -551,6 +551,7 @@ def _scan_line__by_lot(self, picking, lot): or (lot.name == l.lot_name and lot.product_id == l.product_id) ) and not l.shopfloor_unloaded + and l.shopfloor_user_id.id in (False, self.env.uid) ) ) if not lines: @@ -572,7 +573,9 @@ def _scan_line__by_lot(self, picking, lot): def _scan_line__fallback(self, picking, barcode): # We might have lines with no lot, but with a lot_name. lines = picking.move_line_ids.filtered( - lambda l: l.lot_name == barcode and not l.shopfloor_unloaded + lambda l: l.lot_name == barcode + and not l.shopfloor_unloaded + and l.shopfloor_user_id.id in (False, self.env.uid) ) if not lines: return self._response_for_select_move(