diff --git a/shopfloor/models/stock_move.py b/shopfloor/models/stock_move.py index f700f1bec7..c641389dcb 100644 --- a/shopfloor/models/stock_move.py +++ b/shopfloor/models/stock_move.py @@ -24,3 +24,16 @@ def split_other_move_lines(self, move_lines, intersection=False): backorder_move._action_assign() return backorder_move return False + + def _action_done(self, cancel_backorder=False): + # Overloaded to send the email when the last move of a picking is validated. + # The method 'stock.picking._send_confirmation_email' is called only from + # the 'stock.picking.action_done()' method but never when moves are + # validated partially through the current method. + moves = super()._action_done(cancel_backorder) + if not self.env.context.get("_action_done_from_picking"): + pickings = moves.picking_id + for picking in pickings: + if picking.state == "done": + picking._send_confirmation_email() + return moves diff --git a/shopfloor/models/stock_picking.py b/shopfloor/models/stock_picking.py index a47b4d87ec..90e10fd131 100644 --- a/shopfloor/models/stock_picking.py +++ b/shopfloor/models/stock_picking.py @@ -42,3 +42,16 @@ def _create_backorder(self): return self.browse() else: return super()._create_backorder() + + def action_done(self): + self = self.with_context(_action_done_from_picking=True) + return super().action_done() + + def _send_confirmation_email(self): + # Avoid sending the confirmation email twice (one when the + # 'picking.action_done()' is called, and one when the last move of this + # picking is validated through 'move._action_done()') + # We send the confirmation email + if self.env.context.get("_action_done_from_picking"): + return + super()._send_confirmation_email() diff --git a/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py b/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py index b46855f270..f8fd7db29b 100644 --- a/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +++ b/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py @@ -1,3 +1,5 @@ +from unittest import mock + from .test_location_content_transfer_base import LocationContentTransferCommonCase @@ -130,14 +132,18 @@ def test_set_destination_package_dest_location_to_confirm(self): def test_set_destination_package_dest_location_ok(self): """Scanned destination location valid, moves set to done.""" package_level = self.picking1.package_level_ids[0] - response = self.service.dispatch( - "set_destination_package", - params={ - "location_id": self.content_loc.id, - "package_level_id": package_level.id, - "barcode": self.dest_location.barcode, - }, - ) + with mock.patch.object( + type(self.picking1), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination_package", + params={ + "location_id": self.content_loc.id, + "package_level_id": package_level.id, + "barcode": self.dest_location.barcode, + }, + ) + send_confirmation_email.assert_called_once() move_lines = self.service._find_transfer_move_lines(self.content_loc) self.assert_response_start_single( response, @@ -301,15 +307,19 @@ def test_set_destination_line_partial_qty(self): self.assertEqual(move_line_c.move_id.state, "done") # Scan remaining qty (4/10) remaining_move_line_c = move_product_c_splitted.move_line_ids - response = self.service.dispatch( - "set_destination_line", - params={ - "location_id": self.content_loc.id, - "move_line_id": remaining_move_line_c.id, - "quantity": remaining_move_line_c.product_uom_qty, - "barcode": self.dest_location.barcode, - }, - ) + with mock.patch.object( + type(self.picking2), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination_line", + params={ + "location_id": self.content_loc.id, + "move_line_id": remaining_move_line_c.id, + "quantity": remaining_move_line_c.product_uom_qty, + "barcode": self.dest_location.barcode, + }, + ) + send_confirmation_email.assert_not_called() # Check move line data self.assertEqual(remaining_move_line_c.move_id.product_uom_qty, 4) self.assertEqual(remaining_move_line_c.product_uom_qty, 0) @@ -329,20 +339,24 @@ def test_set_destination_line_partial_qty(self): move_line_d = self.picking2.move_line_ids.filtered( lambda m: m.product_id == self.product_d ) - response = self.service.dispatch( - "set_destination_line", - params={ - "location_id": self.content_loc.id, - "move_line_id": move_line_d.id, - "quantity": move_line_d.product_uom_qty, - "barcode": self.dest_location.barcode, - }, - ) - self.assertEqual(move_line_d.move_id.product_uom_qty, 10) - self.assertEqual(move_line_d.product_uom_qty, 0) - self.assertEqual(move_line_d.qty_done, 10) - self.assertEqual(move_line_d.state, "done") - self.assertEqual(self.picking2.state, "done") + with mock.patch.object( + type(self.picking2), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination_line", + params={ + "location_id": self.content_loc.id, + "move_line_id": move_line_d.id, + "quantity": move_line_d.product_uom_qty, + "barcode": self.dest_location.barcode, + }, + ) + self.assertEqual(move_line_d.move_id.product_uom_qty, 10) + self.assertEqual(move_line_d.product_uom_qty, 0) + self.assertEqual(move_line_d.qty_done, 10) + self.assertEqual(move_line_d.state, "done") + self.assertEqual(self.picking2.state, "done") + send_confirmation_email.assert_called_once() class LocationContentTransferSetDestinationXSpecialCase( @@ -506,24 +520,32 @@ def test_set_destination_line_split_move(self): remaining_move_lines = self.picking.move_line_ids_without_package.filtered( lambda ml: ml.state == "assigned" ) - for ml in remaining_move_lines: + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: + for ml in remaining_move_lines: + self.service.dispatch( + "set_destination_line", + params={ + "location_id": self.content_loc.id, + "move_line_id": ml.id, + "quantity": ml.product_uom_qty, + "barcode": self.dest_location.barcode, + }, + ) + self.assertEqual(self.picking.state, "assigned") + send_confirmation_email.assert_not_called() + package_level = self.picking.package_level_ids[0] + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: self.service.dispatch( - "set_destination_line", + "set_destination_package", params={ "location_id": self.content_loc.id, - "move_line_id": ml.id, - "quantity": ml.product_uom_qty, + "package_level_id": package_level.id, "barcode": self.dest_location.barcode, }, ) - self.assertEqual(self.picking.state, "assigned") - package_level = self.picking.package_level_ids[0] - self.service.dispatch( - "set_destination_package", - params={ - "location_id": self.content_loc.id, - "package_level_id": package_level.id, - "barcode": self.dest_location.barcode, - }, - ) - self.assertEqual(self.picking.state, "done") + self.assertEqual(self.picking.state, "done") + send_confirmation_email.assert_called_once() diff --git a/shopfloor/tests/test_single_pack_transfer.py b/shopfloor/tests/test_single_pack_transfer.py index c98b8870cf..3397279830 100644 --- a/shopfloor/tests/test_single_pack_transfer.py +++ b/shopfloor/tests/test_single_pack_transfer.py @@ -1,3 +1,5 @@ +from unittest import mock + from odoo.tests.common import Form from .common import CommonCase @@ -397,13 +399,17 @@ def test_validate(self): # now, call the service to proceed with validation of the # movement - response = self.service.dispatch( - "validate", - params={ - "package_level_id": package_level.id, - "location_barcode": self.shelf2.barcode, - }, - ) + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "validate", + params={ + "package_level_id": package_level.id, + "location_barcode": self.shelf2.barcode, + }, + ) + send_confirmation_email.assert_called_once() self.assert_response( response, @@ -465,13 +471,17 @@ def test_validate_completion_info(self): # now, call the service to proceed with validation of the # movement - response = self.service.dispatch( - "validate", - params={ - "package_level_id": package_level.id, - "location_barcode": self.shelf2.barcode, - }, - ) + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "validate", + params={ + "package_level_id": package_level.id, + "location_barcode": self.shelf2.barcode, + }, + ) + send_confirmation_email.assert_called_once() self.assert_response( response, @@ -595,13 +605,17 @@ def test_validate_location_to_confirm(self): # expected destination is 'shelf2', we'll scan shelf1 which must # ask a confirmation to the user (it's still in the same picking type) - response = self.service.dispatch( - "validate", - params={ - "package_level_id": package_level.id, - "location_barcode": self.shelf1.barcode, - }, - ) + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "validate", + params={ + "package_level_id": package_level.id, + "location_barcode": self.shelf1.barcode, + }, + ) + send_confirmation_email.assert_not_called() message = self.service.actions_for("message").confirm_location_changed( self.shelf2, self.shelf1 @@ -641,15 +655,19 @@ def test_validate_location_with_confirm(self): # expected destination is 'shelf1', we'll scan shelf2 which must # ask a confirmation to the user (it's still in the same picking type) - response = self.service.dispatch( - "validate", - params={ - "package_level_id": package_level.id, - "location_barcode": self.shelf2.barcode, - # acknowledge the change of destination - "confirmation": True, - }, - ) + with mock.patch.object( + type(self.picking), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "validate", + params={ + "package_level_id": package_level.id, + "location_barcode": self.shelf2.barcode, + # acknowledge the change of destination + "confirmation": True, + }, + ) + send_confirmation_email.assert_called_once() self.assert_response( response, diff --git a/shopfloor/tests/test_zone_picking_set_line_destination.py b/shopfloor/tests/test_zone_picking_set_line_destination.py index f1414d6823..da2f0e110f 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination.py @@ -1,3 +1,5 @@ +from unittest import mock + from .test_zone_picking_base import ZonePickingCommonCase @@ -150,17 +152,21 @@ def test_set_destination_location_no_other_move_line_full_qty(self): self.assertEqual(len(moves_before), 1) self.assertEqual(len(moves_before.move_line_ids), 1) move_line = moves_before.move_line_ids - response = self.service.dispatch( - "set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "move_line_id": move_line.id, - "barcode": self.packing_location.barcode, - "quantity": move_line.product_uom_qty, - "confirmation": False, - }, - ) + with mock.patch.object( + type(self.picking1), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "move_line_id": move_line.id, + "barcode": self.packing_location.barcode, + "quantity": move_line.product_uom_qty, + "confirmation": False, + }, + ) + send_confirmation_email.assert_called_once() # Check picking data moves_after = self.picking1.move_lines self.assertEqual(moves_before, moves_after) @@ -200,17 +206,21 @@ def test_set_destination_location_no_other_move_line_partial_qty(self): move_line = moves_before.move_line_ids # we need a destination package if we want to scan a destination location move_line.result_package_id = self.free_package - response = self.service.dispatch( - "set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "move_line_id": move_line.id, - "barcode": barcode, - "quantity": 6, - "confirmation": False, - }, - ) + with mock.patch.object( + type(self.picking3), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "move_line_id": move_line.id, + "barcode": barcode, + "quantity": 6, + "confirmation": False, + }, + ) + send_confirmation_email.assert_not_called() self.assert_response_set_line_destination( response, zone_location, @@ -247,17 +257,21 @@ def test_set_destination_location_several_move_line_full_qty(self): # we need a destination package if we want to scan a destination location move_line.result_package_id = self.free_package other_move_line = moves_before.move_line_ids[1] - response = self.service.dispatch( - "set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "move_line_id": move_line.id, - "barcode": self.packing_location.barcode, - "quantity": move_line.product_uom_qty, # 6 qty - "confirmation": False, - }, - ) + with mock.patch.object( + type(self.picking4), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "move_line_id": move_line.id, + "barcode": self.packing_location.barcode, + "quantity": move_line.product_uom_qty, # 6 qty + "confirmation": False, + }, + ) + send_confirmation_email.assert_not_called() # Check picking data (move has been split in two, 6 done and 4 remaining) moves_after = self.picking4.move_lines self.assertEqual(len(moves_after), 2) @@ -306,17 +320,21 @@ def test_set_destination_location_several_move_line_partial_qty(self): move_line = moves_before.move_line_ids[0] # we need a destination package if we want to scan a destination location move_line.result_package_id = self.free_package - response = self.service.dispatch( - "set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "move_line_id": move_line.id, - "barcode": barcode, - "quantity": 4, # 4/6 qty - "confirmation": False, - }, - ) + with mock.patch.object( + type(self.picking4), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "move_line_id": move_line.id, + "barcode": barcode, + "quantity": 4, # 4/6 qty + "confirmation": False, + }, + ) + send_confirmation_email.assert_not_called() self.assert_response_set_line_destination( response, zone_location, diff --git a/shopfloor/tests/test_zone_picking_unload_all.py b/shopfloor/tests/test_zone_picking_unload_all.py index 86ebbc4ed9..d9ab4eb084 100644 --- a/shopfloor/tests/test_zone_picking_unload_all.py +++ b/shopfloor/tests/test_zone_picking_unload_all.py @@ -1,3 +1,5 @@ +from unittest import mock + from .test_zone_picking_base import ZonePickingCommonCase @@ -159,14 +161,18 @@ def test_set_destination_all_ok(self): another_package, ) # set destination location for all lines in the buffer - response = self.service.dispatch( - "set_destination_all", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "barcode": self.packing_location.barcode, - }, - ) + with mock.patch.object( + type(self.picking5), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "set_destination_all", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "barcode": self.packing_location.barcode, + }, + ) + send_confirmation_email.assert_called_once() # check data self.assertEqual(self.picking5.state, "done") # buffer should be empty diff --git a/shopfloor/tests/test_zone_picking_unload_set_destination.py b/shopfloor/tests/test_zone_picking_unload_set_destination.py index 38492df979..afd3a92465 100644 --- a/shopfloor/tests/test_zone_picking_unload_set_destination.py +++ b/shopfloor/tests/test_zone_picking_unload_set_destination.py @@ -1,3 +1,5 @@ +from unittest import mock + from .test_zone_picking_base import ZonePickingCommonCase @@ -179,16 +181,20 @@ def test_unload_set_destination_ok_buffer_empty(self): move_line.product_uom_qty, self.free_package, ) - response = self.service.dispatch( - "unload_set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "package_id": self.free_package.id, - "barcode": packing_sublocation.barcode, - "confirmation": True, - }, - ) + with mock.patch.object( + type(self.picking1), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "unload_set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "package_id": self.free_package.id, + "barcode": packing_sublocation.barcode, + "confirmation": True, + }, + ) + send_confirmation_email.assert_called_once() # check data self.assertEqual(move_line.location_dest_id, packing_sublocation) self.assertEqual(move_line.move_id.state, "done") @@ -220,15 +226,19 @@ def test_unload_set_destination_ok_buffer_not_empty(self): package_dest, ) # process 1/2 buffer line - response = self.service.dispatch( - "unload_set_destination", - params={ - "zone_location_id": zone_location.id, - "picking_type_id": picking_type.id, - "package_id": self.free_package.id, - "barcode": self.packing_location.barcode, - }, - ) + with mock.patch.object( + type(self.picking5), "_send_confirmation_email" + ) as send_confirmation_email: + response = self.service.dispatch( + "unload_set_destination", + params={ + "zone_location_id": zone_location.id, + "picking_type_id": picking_type.id, + "package_id": self.free_package.id, + "barcode": self.packing_location.barcode, + }, + ) + send_confirmation_email.assert_not_called() # check data move_line = self.picking5.move_line_ids.filtered( lambda l: l.result_package_id == self.free_package