diff --git a/shopfloor_reception/data/shopfloor_scenario_data.xml b/shopfloor_reception/data/shopfloor_scenario_data.xml index d9ef7cf264..639e3f66f1 100644 --- a/shopfloor_reception/data/shopfloor_scenario_data.xml +++ b/shopfloor_reception/data/shopfloor_scenario_data.xml @@ -9,7 +9,8 @@ { "auto_post_line": true, - "allow_return": true + "allow_return": true, + "allow_select_document_by_product": true } diff --git a/shopfloor_reception/models/__init__.py b/shopfloor_reception/models/__init__.py index ae4c27227f..7d45a954fd 100644 --- a/shopfloor_reception/models/__init__.py +++ b/shopfloor_reception/models/__init__.py @@ -1 +1,2 @@ from . import stock_picking +from . import shopfloor_menu diff --git a/shopfloor_reception/models/shopfloor_menu.py b/shopfloor_reception/models/shopfloor_menu.py new file mode 100644 index 0000000000..625f4ee606 --- /dev/null +++ b/shopfloor_reception/models/shopfloor_menu.py @@ -0,0 +1,28 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + +ALLOW_SELECT_DOCUMENT_BY_PRODUCT_HELP = """ +If enabled, users will be able to select transfers by product. +""" + + +class ShopfloorMenu(models.Model): + _inherit = "shopfloor.menu" + + allow_select_document_by_product = fields.Boolean( + string="Allow select document by product", + default=False, + help=ALLOW_SELECT_DOCUMENT_BY_PRODUCT_HELP, + ) + allow_select_document_by_product_is_possible = fields.Boolean( + compute="_compute_pick_pack_same_time_is_possible" + ) + + @api.depends("scenario_id") + def _compute_pick_pack_same_time_is_possible(self): + for menu in self: + menu.allow_select_document_by_product_is_possible = ( + menu.scenario_id.has_option("allow_select_document_by_product") + ) diff --git a/shopfloor_reception/services/reception.py b/shopfloor_reception/services/reception.py index f1d421deb2..c6f99baeda 100644 --- a/shopfloor_reception/services/reception.py +++ b/shopfloor_reception/services/reception.py @@ -256,13 +256,8 @@ def _scan_line__find_or_create_line(self, picking, move, qty_done=1): ) ) ) - if line: - # The line quantity to do needs to correspond to - # the remaining quantity to do of its move. - line.product_uom_qty = move.product_uom_qty - move.quantity_done - else: - qty_todo_remaining = max(0, move.product_uom_qty - move.quantity_done) - values = move._prepare_move_line_vals(quantity=qty_todo_remaining) + if not line: + values = move._prepare_move_line_vals() line = self.env["stock.move.line"].create(values) return self._scan_line__assign_user(picking, line, qty_done) @@ -749,7 +744,7 @@ def _response_for_set_lot(self, picking, line, message=None): message=message, ) - def _align_product_uom_qties(self, move): + def _align_display_product_uom_qty(self, line, response): # This method aligns product uom qties on move lines. # In the shopfloor context, we might have multiple users working at # the same time on the same move. This is done by creating one move line @@ -768,25 +763,32 @@ def _align_product_uom_qties(self, move): # If move is already done, do not update lines qties # if move.state in ("done", "cancel"): # return - + move = line.move_id qty_todo = move.product_uom_qty - qty_done = sum(move.move_line_ids.mapped("qty_done")) + qty_done = 0.0 + move_uom = move.product_uom + for move_line in move.move_line_ids: + # Use move's uom + qty_done += move_uom._compute_quantity( + move_line.qty_done, move_line.product_uom_id, round=False + ) rounding = move.product_id.uom_id.rounding compare = float_compare(qty_done, qty_todo, precision_rounding=rounding) - if compare < 1: # If qty done <= qty todo, align qty todo on move lines + if compare < 1: # If qty done < qty todo, align qty todo in the response remaining_todo = qty_todo - qty_done - # if we didn't bypass reservation update, the quant reservation - # would be reduced as much as the deduced quantity, which is wrong - # as we only moved the quantity to a new move line - lines = move.move_line_ids.with_context(bypass_reservation_update=True) - for line in lines: - line.product_uom_qty = line.qty_done + remaining_todo + # Change back to line uom + line_todo = line.product_uom_id._compute_quantity( + line.qty_done + remaining_todo, move_uom, round=False + ) + response["data"]["set_quantity"]["selected_move_line"][0][ + "quantity" + ] = line_todo + return response def _response_for_set_quantity( self, picking, line, message=None, asking_confirmation=False ): - self._align_product_uom_qties(line.move_id) - return self._response( + response = self._response( next_state="set_quantity", data={ "selected_move_line": self._data_for_move_lines(line), @@ -795,6 +797,7 @@ def _response_for_set_quantity( }, message=message, ) + return self._align_display_product_uom_qty(line, response) def _response_for_set_destination(self, picking, line, message=None): return self._response( @@ -839,7 +842,12 @@ def start(self): def _scan_document__get_handlers_by_type(self): return { "picking": self._scan_document__by_picking, - "product": self._scan_document__by_product, + # only add the handler if allow_select_document_by_product is enabled + "product": ( + self._scan_document__by_product + if self.work.menu.allow_select_document_by_product + else None + ), "packaging": self._scan_document__by_packaging, "lot": self._scan_document__by_lot, "origin_move": self._scan_document__by_origin_move, @@ -1147,6 +1155,22 @@ def set_quantity( ) return self._response_for_set_quantity(picking, selected_line) + def set_quantity__cancel_action(self, picking_id, selected_line_id): + picking = self.env["stock.picking"].browse(picking_id) + selected_line = self.env["stock.move.line"].browse(selected_line_id) + message = self._check_picking_status(picking) + if message: + return self._response_for_set_quantity( + picking, selected_line, message=message + ) + if selected_line.exists(): + if selected_line.product_uom_qty: + stock = self._actions_for("stock") + stock.unmark_move_line_as_picked(selected_line) + else: + selected_line.unlink() + return self._response_for_select_move(picking) + def _set_quantity__process__set_qty_and_split(self, picking, line, quantity): move = line.move_id sum(move.move_line_ids.mapped("qty_done")) @@ -1216,6 +1240,7 @@ 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): + selected_line.product_uom_qty = selected_line.qty_done if ( selected_line.picking_id.is_shopfloor_created and self.work.menu.allow_return @@ -1237,12 +1262,20 @@ def _post_shopfloor_created_line(self, selected_line): ) def _auto_post_line(self, selected_line): + # If user only processed 1/5 and is the only one working on the move, + # then selected_line is the only one related to this move. + # In such case, we must ensure there's another move line with the remaining + # quantity to do, so selected_line is extracted in a new move as expected. + new_move_line = selected_line._split_partial_quantity() new_move = selected_line.move_id.split_other_move_lines( selected_line, intersection=True ) if new_move: # A new move is created in case of partial quantity new_move.extract_and_action_done() + stock = self._actions_for("stock") + stock.unmark_move_line_as_picked(new_move_line) + new_move_line.location_dest_id = new_move_line.move_id.location_dest_id return # In case of full quantity, post the initial move selected_line.move_id.extract_and_action_done() @@ -1411,6 +1444,16 @@ def set_quantity(self): "confirmation": {"type": "boolean"}, } + def set_quantity__cancel_action(self): + return { + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "selected_line_id": { + "coerce": to_int, + "type": "integer", + "required": True, + }, + } + def process_with_existing_pack(self): return { "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, @@ -1542,6 +1585,9 @@ def _set_lot_next_states(self): def _set_quantity_next_states(self): return {"set_quantity", "select_move", "set_destination"} + def _set_quantity__cancel_action_next_states(self): + return {"set_quantity", "select_move"} + def _set_destination_next_states(self): return {"set_destination", "select_move"} @@ -1622,6 +1668,16 @@ def _schema_set_quantity(self): }, } + @property + def _schema_set_quantity__cancel_action(self): + return { + "selected_move_line": { + "type": "list", + "schema": {"type": "dict", "schema": self.schemas.move_line()}, + }, + "picking": {"type": "dict", "schema": self.schemas.picking()}, + } + @property def _schema_set_destination(self): return { @@ -1699,6 +1755,11 @@ def set_lot_confirm_action(self): def set_quantity(self): return self._response_schema(next_states=self._set_quantity_next_states()) + def set_quantity__cancel_action(self): + return self._response_schema( + next_states=self._set_quantity__cancel_action_next_states() + ) + def process_with_existing_pack(self): return self._response_schema( next_states=self._process_with_existing_pack_next_states() diff --git a/shopfloor_reception/tests/common.py b/shopfloor_reception/tests/common.py index 73da099b67..6811f10a71 100644 --- a/shopfloor_reception/tests/common.py +++ b/shopfloor_reception/tests/common.py @@ -49,6 +49,7 @@ def setUpClassVars(cls, *args, **kwargs): cls.profile = cls.env.ref("shopfloor.profile_demo_1") cls.picking_type = cls.menu.picking_type_ids cls.wh = cls.picking_type.warehouse_id + cls._enable_allow_select_document_by_product() def _data_for_move_lines(self, lines, **kw): return self.data.move_lines(lines, **kw) @@ -138,3 +139,34 @@ def _get_today_pickings(self): ], order="scheduled_date ASC", ) + + @classmethod + def _enable_allow_select_document_by_product(cls): + cls.menu.sudo().allow_select_document_by_product = True + + def assertMessage(self, response, expected_message): + message = response.get("message") + for key, value in expected_message.items(): + self.assertEqual(message.get(key), value) + + @classmethod + def _get_move_ids_from_response(cls, response): + state = response.get("next_state") + data = response["data"][state] + picking_data = data.get("pickings") or [data.get("picking")] + moves_data = [] + for picking in picking_data: + moves_data.extend(picking["moves"]) + return [move["id"] for move in moves_data] + + def _get_service_for_user(self, user): + user_env = self.env(user=user) + return self.get_service( + "reception", menu=self.menu, profile=self.profile, env=user_env + ) + + @classmethod + def _shopfloor_manager_values(cls): + vals = super()._shopfloor_manager_values() + vals["groups_id"] = [(6, 0, [cls.env.ref("stock.group_stock_user").id])] + return vals diff --git a/shopfloor_reception/tests/test_return_scan_line.py b/shopfloor_reception/tests/test_return_scan_line.py index cb5a218048..859f0ea12b 100644 --- a/shopfloor_reception/tests/test_return_scan_line.py +++ b/shopfloor_reception/tests/test_return_scan_line.py @@ -88,15 +88,18 @@ def test_scan_packaging_in_delivery(self): "barcode": self.product_a_packaging.barcode, }, ) - data = self.data.picking(return_picking) selected_move_line = self.get_new_move_lines() + move_line_data = self.data.move_lines(selected_move_line) + move_line_data[0]["quantity"] = 20.0 + # Displayed qtu todo is modified by _align_display_product_uom_qty + data = self.data.picking(return_picking) self.assert_response( response, next_state="set_quantity", data={ "confirmation_required": False, "picking": data, - "selected_move_line": self.data.move_lines(selected_move_line), + "selected_move_line": move_line_data, }, ) self.assertEqual(selected_move_line.qty_done, self.product_a_packaging.qty) diff --git a/shopfloor_reception/tests/test_select_move.py b/shopfloor_reception/tests/test_select_move.py index ea50f0f365..f7899f3c1e 100644 --- a/shopfloor_reception/tests/test_select_move.py +++ b/shopfloor_reception/tests/test_select_move.py @@ -209,9 +209,20 @@ def test_assign_shopfloor_user_to_line(self): self.assertEqual(other_move_line.shopfloor_user_id.id, False) def test_create_new_line_none_available(self): - # If all lines for a product are already assigned to a different user - # and there's still qty todo remaining - # a new line will be created for that qty todo. + # If there's already a move line for a given incoming move, + # we assigned the whole move's product_uom_qty to it. + # The reason for that is that when recomputing states for a given move + # if sum(move.move_line_ids.product_uom_qty) != move.product_uom_qty, + # then it's state won't be assigned. + # For instance: + # - user 1 selects line1 + # - user 2 selected line1 too + # - user 1 posts 20/40 goods + # - user 2 tries to process any qty, and it fails, because posting + # a move triggers the recompute of move's state + # To avoid that, the first created line gets + # product_uom_qty = move.product_uom_qty + # The next ones are getting 0. picking = self._create_picking() self.assertEqual(len(picking.move_line_ids), 2) selected_move_line = picking.move_line_ids.filtered( @@ -233,9 +244,11 @@ def test_create_new_line_none_available(self): "barcode": self.product_a.barcode, }, ) + # A new line has been created self.assertEqual(len(picking.move_line_ids), 3) created_line = picking.move_line_ids[2] - self.assertEqual(created_line.product_uom_qty, 7) + # And its product_uom_qty is 0 + self.assertEqual(created_line.product_uom_qty, 0.0) self.assertEqual(created_line.shopfloor_user_id.id, self.env.uid) def test_done_action(self): diff --git a/shopfloor_reception/tests/test_set_quantity.py b/shopfloor_reception/tests/test_set_quantity.py index a3b08ad5b3..25c3285d0c 100644 --- a/shopfloor_reception/tests/test_set_quantity.py +++ b/shopfloor_reception/tests/test_set_quantity.py @@ -382,18 +382,6 @@ def test_scan_new_package(self): }, ) - @classmethod - def _shopfloor_manager_values(cls): - vals = super()._shopfloor_manager_values() - vals["groups_id"] = [(6, 0, [cls.env.ref("stock.group_stock_user").id])] - return vals - - def _get_service_for_user(self, user): - user_env = self.env(user=user) - return self.get_service( - "reception", menu=self.menu, profile=self.profile, env=user_env - ) - def test_concurrent_update(self): # We're testing that move line's product uom qties are updated correctly # when users are workng on the same move in parallel @@ -408,6 +396,10 @@ def test_concurrent_update(self): ) self.assertEqual(len(selected_move_line), 1) self.assertEqual(selected_move_line.qty_done, 1.0) + self.assertEqual( + selected_move_line.product_uom_qty, + selected_move_line.move_id.product_uom_qty, + ) # Let's make the first user work a little bit, and pick a total of 4 units for __ in range(4): @@ -431,10 +423,13 @@ def test_concurrent_update(self): "scan_line", params={"picking_id": picking.id, "barcode": self.product_a.barcode}, ) + # The whole move's product_uom_qty has been assigned to the first created line. + # The new one gets 0.0 new_line = picking.move_line_ids.filtered( lambda l: l.product_id == self.product_a and l.shopfloor_user_id == manager_user ) + self.assertEqual(new_line.product_uom_qty, 0.0) move_lines = selected_move_line | new_line line_service_mapping = [ @@ -448,10 +443,6 @@ def test_concurrent_update(self): self.assertEqual(lines_qty_done, 6.0) # should be equal to the moves quantity_done self.assertEqual(lines_qty_done, move_lines.move_id.quantity_done) - # And also, the remaining qty is 4.0, then for each move line, - # product_uom_qty = line.qty_done + move's remaining_qty - for line in move_lines: - self.assertEqual(line.product_uom_qty, line.qty_done + 4.0) # Now, let the new user finish its work for __ in range(4): @@ -470,10 +461,12 @@ def test_concurrent_update(self): self.assertEqual(lines_qty_done, 10.0) self.assertEqual(lines_qty_done, move_lines.move_id.quantity_done) - # And also, the product_uom_qty should be == qty_done == 5.0 - for line in move_lines: - self.assertEqual(line.product_uom_qty, 5.0) - self.assertEqual(line.product_uom_qty, line.qty_done) + # However, product_uom_qty hasn't changed + self.assertEqual(selected_move_line.product_uom_qty, 10.0) + self.assertEqual(new_line.product_uom_qty, 0.0) + # And what's important is that the sum of lines's product_uom_qty is + # always == move's product_uom_qty + self.assertEqual(sum(move_lines.mapped("product_uom_qty")), 10.0) # However, if we pick more than move's product_uom_qty, then lines # product_uom_qty isn't updated, in order to be able to display an error @@ -536,10 +529,11 @@ def test_concurrent_update(self): ) self.assertEqual(selected_move_line.qty_done, 3.0) - self.assertEqual(selected_move_line.product_uom_qty, 3.0) - self.assertEqual(new_line.qty_done, 7.0) - self.assertEqual(new_line.product_uom_qty, 7.0) + + self.assertEqual(selected_move_line.product_uom_qty, 10.0) + self.assertEqual(new_line.product_uom_qty, 0.0) + self.assertEqual(sum(move_lines.mapped("product_uom_qty")), 10.0) # And everything's fine on the move move = move_lines.move_id @@ -590,3 +584,199 @@ def test_split_move_line(self): lambda l: l.product_id == self.product_a ) self.assertEqual(len(move_lines), 3) + + def test_concurrent_update_2(self): + self.menu.sudo().auto_post_line = True + self.input_location.sudo().active = True + # Test related to picking being set to "ready" once the first user posts + # its move line, hence the picking being not available in shopfloor afterwards. + + # The reason for that is that `_post_line` calls `_recompute_state`. + # If at this point there's more or less reserved qty than what's been ordered + # then state isn't computed as assigned. + + # This test ensure that this isn't the case anymore. + + # Creating the picking, selecting the move line. + picking = self._create_picking() + service_user_1 = self.service + service_user_1.dispatch("scan_document", params={"barcode": picking.name}) + service_user_1.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + move_line_user_1 = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + ) + # The only move line should have qty_done = 1 + self.assertEqual(move_line_user_1.qty_done, 1.0) + self.assertEqual(move_line_user_1.product_uom_qty, 10.0) + + # Now, concurrently pick products with another user for the same move + manager_user = self.shopfloor_manager + service_user_2 = self._get_service_for_user(manager_user) + service_user_2.dispatch("scan_document", params={"barcode": picking.name}) + service_user_2.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + # The whole move's product_uom_qty has been assigned to the first created line. + # The new one gets 0.0 + move_line_user_2 = picking.move_line_ids.filtered( + lambda l: l.product_id == self.product_a + and l.shopfloor_user_id == manager_user + ) + self.assertEqual(move_line_user_2.product_uom_qty, 0.0) + self.assertEqual(move_line_user_2.qty_done, 1.0) + + # At this point, both lines are referencing the same move + self.assertEqual(move_line_user_2.move_id, move_line_user_1.move_id) + + # A new move / picking will be created after it is posted. + # store the list of pickings to find it out after it is posted + # moves before + lines_before = self.env["stock.move.line"].search([]) + + # Now, post user_1 move line + response = service_user_1.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "quantity": move_line_user_1.qty_done, + }, + ) + picking_data = self.data.picking(picking) + self.assert_response( + response, + next_state="set_destination", + data={ + "picking": picking_data, + "selected_move_line": self.data.move_lines(move_line_user_1), + }, + ) + + response = self.service.dispatch( + "set_destination", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "location_name": self.input_location.name, + }, + ) + lines_after = self.env["stock.move.line"].search( + [("id", "not in", lines_before.ids)] + ) + # After move_line is posted, its state is done, and its qty_done is 1.0 + self.assertEqual(move_line_user_1.state, "done") + + # The remaining one is still assigned + self.assertEqual(move_line_user_2.state, "assigned") + # As well as the new one + self.assertEqual(len(lines_after), 1) + + # And the total remaining qty to be done is 9.0 (10.0 - 1.0) + self.assertEqual( + lines_after.product_uom_qty + move_line_user_2.product_uom_qty, 9.0 + ) + + def test_move_states(self): + # as only assigned moves can be posted, we need to ensure that + # we got the right states in any case, especially when users are working + # concurrently + picking = self._create_picking() + move_product_a = picking.move_lines.filtered( + lambda l: l.product_id == self.product_a + ) + # user1 processes 10 units + move_line_user_1 = move_product_a.move_line_ids + service_user_1 = self.service + service_user_1.dispatch("scan_document", params={"barcode": picking.name}) + service_user_1.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + response = service_user_1.dispatch( + "set_quantity", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "quantity": move_product_a.product_qty - 1, + "barcode": self.product_a.barcode, + }, + ) + # user2 selects the same picking + user2 = self.shopfloor_manager + service_user_2 = self._get_service_for_user(user2) + response = service_user_2.dispatch( + "scan_document", params={"barcode": picking.name} + ) + # And the same line + service_user_2.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + move_line_user_2 = move_product_a.move_line_ids - move_line_user_1 + # user1 shouldn't be able to process his move, since + # move qty_done > move product_qty + response = service_user_1.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "quantity": 10.0, + }, + ) + # + expected_message = { + "body": "You cannot process that much units.", + "message_type": "error", + } + self.assertMessage(response, expected_message) + # user1 cancels the operation + service_user_1.dispatch( + "set_quantity__cancel_action", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + }, + ) + self.assertFalse(move_line_user_1.shopfloor_user_id) + self.assertEqual(move_line_user_1.qty_done, 0) + # User2 should be able to process 1 unit + response = service_user_2.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_2.id, + "quantity": 1.0, + }, + ) + data = self.data.picking(picking) + self.assert_response( + response, + next_state="set_destination", + data={ + "picking": data, + "selected_move_line": self.data.move_lines(move_line_user_2), + }, + ) + self.assertEqual(move_product_a.quantity_done, 1.0) + response = service_user_2.dispatch( + "set_destination", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_2.id, + "location_name": self.dispatch_location.name, + }, + ) + # When posted, the move line product_uom_qty has been set to qty_done + self.assertEqual(move_line_user_2.qty_done, move_line_user_2.product_qty) + self.assert_response( + response, next_state="select_move", data=self._data_for_select_move(picking) + ) + # Now, user1 can start working on this again + service_user_1.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) diff --git a/shopfloor_reception/tests/test_set_quantity_action.py b/shopfloor_reception/tests/test_set_quantity_action.py index 7cf981eacc..8881e64a5f 100644 --- a/shopfloor_reception/tests/test_set_quantity_action.py +++ b/shopfloor_reception/tests/test_set_quantity_action.py @@ -84,3 +84,93 @@ def test_process_without_package(self): }, ) self.assertFalse(self.selected_move_line.result_package_id) + + def test_cancel_action(self): + picking = self._create_picking() + move_product_a = picking.move_lines.filtered( + lambda l: l.product_id == self.product_a + ) + # User 1 and 2 selects the same picking + service_user_1 = self.service + service_user_1.dispatch("scan_document", params={"barcode": picking.name}) + user2 = self.shopfloor_manager + service_user_2 = self._get_service_for_user(user2) + response = service_user_2.dispatch( + "scan_document", params={"barcode": picking.name} + ) + # both users selects the same move + service_user_1.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + move_line_user_1 = move_product_a.move_line_ids + service_user_2.dispatch( + "scan_line", + params={"picking_id": picking.id, "barcode": self.product_a.barcode}, + ) + move_line_user_2 = move_product_a.move_line_ids - move_line_user_1 + # And both sets the qty done to 10 + service_user_1.dispatch( + "set_quantity", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "quantity": 10, + }, + ) + service_user_2.dispatch( + "set_quantity", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_2.id, + "quantity": 10, + }, + ) + # Users are blocked, product_uom_qty is 10, but both users have qty_done=10 + # on their move line, therefore, none of them can confirm + expected_message = { + "body": "You cannot process that much units.", + "message_type": "error", + } + response = service_user_1.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + "quantity": 10.0, + }, + ) + self.assertMessage(response, expected_message) + response = service_user_2.dispatch( + "process_with_new_pack", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_2.id, + "quantity": 10.0, + }, + ) + self.assertMessage(response, expected_message) + # make user1 cancel + service_user_1.dispatch( + "set_quantity__cancel_action", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_1.id, + }, + ) + # Since we reused the move line created by odoo for the first user, we only + # reset the line + self.assertTrue(move_line_user_1.exists()) + self.assertFalse(move_line_user_1.shopfloor_user_id) + self.assertEqual(move_line_user_1.qty_done, 0) + self.assertEqual(move_line_user_1.product_uom_qty, 10) + # make user cancel + service_user_2.dispatch( + "set_quantity__cancel_action", + params={ + "picking_id": picking.id, + "selected_line_id": move_line_user_2.id, + }, + ) + # This line has been created by shopfloor, therefore, we unlinked it + self.assertFalse(move_line_user_2.exists()) diff --git a/shopfloor_reception_mobile/static/src/scenario/reception.js b/shopfloor_reception_mobile/static/src/scenario/reception.js index f366d422d9..38d2531eca 100644 --- a/shopfloor_reception_mobile/static/src/scenario/reception.js +++ b/shopfloor_reception_mobile/static/src/scenario/reception.js @@ -153,6 +153,7 @@ const Reception = { + diff --git a/shopfloor_reception_mobile/static/src/scenario/reception_states.js b/shopfloor_reception_mobile/static/src/scenario/reception_states.js index fca90f6e80..4a2438a87d 100644 --- a/shopfloor_reception_mobile/static/src/scenario/reception_states.js +++ b/shopfloor_reception_mobile/static/src/scenario/reception_states.js @@ -171,6 +171,7 @@ export const reception_states = function () { events: { qty_edit: "on_qty_edit", go_back: "on_back", + cancel: "on_cancel", }, on_qty_edit: (qty) => { this.scan_destination_qty = parseInt(qty, 10); @@ -188,14 +189,12 @@ export const reception_states = function () { ); }, on_cancel: () => { - // TODO: this endpoing is currently missing in the backend, - // and it's currently in the roadmap. - // Once it's implemented, uncomment this call. - // this.wait_call( - // this.odoo.call("cancel", { - // package_level_id: this.state.data.id, - // }) - // ); + this.wait_call( + this.odoo.call("set_quantity__cancel_action", { + picking_id: this.state.data.picking.id, + selected_line_id: this.line_being_handled.id, + }) + ); }, on_add_to_existing_pack: () => { this.wait_call(