From 037be0e6e3c63dc64dc0391fe46509c14e800e90 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Fri, 4 Aug 2023 15:38:04 +0200 Subject: [PATCH 01/11] shopfloor: imp zone_picking confirmation --- shopfloor/services/zone_picking.py | 57 ++++++++++--------- shopfloor/tests/test_zone_picking_base.py | 16 +++--- ...est_zone_picking_complete_mix_pack_flux.py | 2 +- .../tests/test_zone_picking_select_line.py | 4 +- .../test_zone_picking_set_line_destination.py | 40 ++++++------- ...et_line_destination_package_not_allowed.py | 6 +- ..._picking_set_line_destination_pick_pack.py | 6 +- .../tests/test_zone_picking_unload_all.py | 2 +- ...est_zone_picking_unload_set_destination.py | 8 +-- 9 files changed, 69 insertions(+), 72 deletions(-) diff --git a/shopfloor/services/zone_picking.py b/shopfloor/services/zone_picking.py index 716be37b53..12fe30f338 100644 --- a/shopfloor/services/zone_picking.py +++ b/shopfloor/services/zone_picking.py @@ -204,7 +204,7 @@ def _response_for_select_line( move_lines, message=None, popup=None, - confirmation_required=False, + confirmation_required=None, product=False, sublocation=False, package=False, @@ -227,7 +227,7 @@ def _response_for_set_line_destination( self, move_line, message=None, - confirmation_required=False, + confirmation_required=None, **kw, ): if confirmation_required and not message: @@ -265,7 +265,7 @@ def _response_for_unload_all( self, move_lines, message=None, - confirmation_required=False, + confirmation_required=None, ): if confirmation_required and not message: message = self.msg_store.need_confirmation() @@ -288,7 +288,7 @@ def _response_for_unload_set_destination( self, move_line, message=None, - confirmation_required=False, + confirmation_required=None, ): if confirmation_required and not message: message = self.msg_store.need_confirmation() @@ -527,7 +527,7 @@ def _list_move_lines( def _scan_source_location( self, barcode, - confirmation=False, + confirmation=None, product_id=False, sublocation=False, package=False, @@ -597,7 +597,7 @@ def _find_product_in_location(self, location, product_id, package=False): def _scan_source_package( self, barcode, - confirmation=False, + confirmation=None, product_id=False, sublocation=False, package=False, @@ -660,10 +660,10 @@ def _scan_source_package( product=product, ) if move_lines: - if not confirmation: + if confirmation != barcode: message = self.msg_store.package_different_change() response = self._response_for_select_line( - move_lines, message, confirmation_required=True + move_lines, message, confirmation_required=barcode ) else: change_package_lot = self._actions_for("change.package.lot") @@ -696,7 +696,7 @@ def _get_prefill_qty(self, move_line, qty=0): def _scan_source_product( self, barcode, - confirmation=False, + confirmation=None, product_id=False, sublocation=False, package=False, @@ -758,7 +758,7 @@ def _scan_source_product( def _scan_source_lot( self, barcode, - confirmation=False, + confirmation=None, product_id=False, sublocation=False, package=False, @@ -811,7 +811,7 @@ def _scan_source_lot( def scan_source( self, barcode, - confirmation=False, + confirmation=None, product_id=None, sublocation_id=None, package_id=None, @@ -889,14 +889,14 @@ def scan_source( ) def _set_destination_location( - self, move_line, package, quantity, confirmation, location + self, move_line, package, quantity, confirmation, location, barcode ): location_changed = False response = None # A valid location is a sub-location of the original destination, or a # any sub-location of the picking type's default destination location - # if `confirmation is True + # if `confirmation is equal to the barcode scanned. # Ask confirmation to the user if the scanned location is not in the # expected ones but is valid (in picking type's default destination) if not self.is_dest_location_valid(move_line.move_id, location): @@ -907,7 +907,7 @@ def _set_destination_location( ) return (location_changed, response) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( move_line.location_dest_id, location ): response = self._response_for_set_line_destination( @@ -915,7 +915,7 @@ def _set_destination_location( message=self.msg_store.confirm_location_changed( move_line.location_dest_id, location ), - confirmation_required=True, + confirmation_required=barcode, qty_done=quantity, ) return (location_changed, response) @@ -1081,8 +1081,8 @@ def set_destination( move_line_id, barcode, quantity, - confirmation=False, handle_complete_mix_pack=False, + confirmation=None, ): """Set a destination location (and done) or a destination package (in buffer) @@ -1186,6 +1186,7 @@ def set_destination( quantity, confirmation, location, + barcode, ) if response: if extra_message: @@ -1509,7 +1510,7 @@ def _set_destination_all_response(self, buffer_lines, message=None): return self._response_for_select_line(move_lines, message=message) return self._response_for_start(message=message) - def set_destination_all(self, barcode, confirmation=False): + def set_destination_all(self, barcode, confirmation=None): """Set the destination for all the lines in the buffer Look in ``prepare_unload`` for the definition of the buffer. @@ -1551,7 +1552,7 @@ def set_destination_all(self, barcode, confirmation=False): # destination set on buffer lines # - To confirm if the scanned destination is not a child of the # current destination set on buffer lines - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( buffer_lines.location_dest_id, location ): return self._response_for_unload_all( @@ -1559,7 +1560,7 @@ def set_destination_all(self, barcode, confirmation=False): message=self.msg_store.confirm_location_changed( first(buffer_lines.location_dest_id), location ), - confirmation_required=True, + confirmation_required=barcode, ) # the scanned location is still valid, use it self._write_destination_on_lines(buffer_lines, location) @@ -1661,7 +1662,7 @@ def _lock_lines(self, lines): """Lock move lines""" self._actions_for("lock").for_update(lines) - def unload_set_destination(self, package_id, barcode, confirmation=False): + def unload_set_destination(self, package_id, barcode, confirmation=None): """Scan the final destination for move lines in the buffer with the destination package @@ -1703,7 +1704,7 @@ def unload_set_destination(self, package_id, barcode, confirmation=False): # destination set on buffer lines # - To confirm if the scanned destination is not a child of the # current destination set on buffer lines - if not confirmation and self.is_dest_location_to_confirm( + if not confirmation == barcode and self.is_dest_location_to_confirm( buffer_lines.location_dest_id, location ): return self._response_for_unload_set_destination( @@ -1711,7 +1712,7 @@ def unload_set_destination(self, package_id, barcode, confirmation=False): message=self.msg_store.confirm_location_changed( first(buffer_lines.location_dest_id), location ), - confirmation_required=True, + confirmation_required=barcode, ) # the scanned location is valid, use it self._write_destination_on_lines(buffer_lines, location) @@ -1767,7 +1768,7 @@ def list_move_lines(self): def scan_source(self): return { "barcode": {"required": False, "nullable": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, "product_id": {"required": False, "nullable": True, "type": "integer"}, "sublocation_id": {"required": False, "nullable": True, "type": "integer"}, "package_id": {"required": False, "nullable": True, "type": "integer"}, @@ -1782,7 +1783,7 @@ def set_destination(self): "required": True, "type": "float", }, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, "handle_complete_mix_pack": { "type": "boolean", "nullable": True, @@ -1813,7 +1814,7 @@ def prepare_unload(self): def set_destination_all(self): return { "barcode": {"required": False, "nullable": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } def unload_split(self): @@ -1829,7 +1830,7 @@ def unload_set_destination(self): return { "package_id": {"coerce": to_int, "required": True, "type": "integer"}, "barcode": {"required": False, "nullable": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } @@ -1974,7 +1975,7 @@ def _schema_for_move_line(self): required=False, ), "confirmation_required": { - "type": "boolean", + "type": "string", "nullable": True, "required": False, }, @@ -2005,7 +2006,7 @@ def _schema_for_move_lines(self): self.schemas.move_line(with_picking=True) ), "confirmation_required": { - "type": "boolean", + "type": "string", "nullable": True, "required": False, }, diff --git a/shopfloor/tests/test_zone_picking_base.py b/shopfloor/tests/test_zone_picking_base.py index 8062897a32..6c9d008b3b 100644 --- a/shopfloor/tests/test_zone_picking_base.py +++ b/shopfloor/tests/test_zone_picking_base.py @@ -306,7 +306,7 @@ def _assert_response_select_line( move_lines, message=None, popup=None, - confirmation_required=False, + confirmation_required=None, product=None, sublocation=None, location_first=None, @@ -349,7 +349,7 @@ def assert_response_select_line( move_lines, message=None, popup=None, - confirmation_required=False, + confirmation_required=None, product=None, sublocation=None, location_first=False, @@ -378,7 +378,7 @@ def _assert_response_set_line_destination( picking_type, move_line, message=None, - confirmation_required=False, + confirmation_required=None, qty_done=None, handle_complete_mix_pack=False, ): @@ -409,7 +409,7 @@ def assert_response_set_line_destination( picking_type, move_line, message=None, - confirmation_required=False, + confirmation_required=None, qty_done=None, handle_complete_mix_pack=False, ): @@ -508,7 +508,7 @@ def _assert_response_unload_set_destination( picking_type, move_line, message=None, - confirmation_required=False, + confirmation_required=None, ): self.assert_response( response, @@ -529,7 +529,7 @@ def assert_response_unload_set_destination( picking_type, move_line, message=None, - confirmation_required=False, + confirmation_required=None, ): self._assert_response_unload_set_destination( "unload_set_destination", @@ -549,7 +549,7 @@ def _assert_response_unload_all( picking_type, move_lines, message=None, - confirmation_required=False, + confirmation_required=None, ): self.assert_response( response, @@ -570,7 +570,7 @@ def assert_response_unload_all( picking_type, move_lines, message=None, - confirmation_required=False, + confirmation_required=None, ): self._assert_response_unload_all( "unload_all", diff --git a/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py b/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py index a1d7f6cbce..33effa4587 100644 --- a/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py +++ b/shopfloor/tests/test_zone_picking_complete_mix_pack_flux.py @@ -43,7 +43,7 @@ def test_scan_source_and_set_destination_on_mixed_package(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": quantity_done, - "confirmation": True, + "confirmation": self.packing_location.barcode, "handle_complete_mix_pack": True, }, ) diff --git a/shopfloor/tests/test_zone_picking_select_line.py b/shopfloor/tests/test_zone_picking_select_line.py index b673d59510..84d041e5e2 100644 --- a/shopfloor/tests/test_zone_picking_select_line.py +++ b/shopfloor/tests/test_zone_picking_select_line.py @@ -332,13 +332,13 @@ def test_scan_source_barcode_package_can_replace_in_line(self): picking_type=self.picking_type, move_lines=move_lines, message=self.service.msg_store.package_different_change(), - confirmation_required=True, + confirmation_required=package1b.name, ) self.assertEqual(self.picking1.package_level_ids[0].package_id, package1) # 2nd scan response = self.service.dispatch( "scan_source", - params={"barcode": package1b.name, "confirmation": True}, + params={"barcode": package1b.name, "confirmation": package1b.name}, ) self.assert_response_set_line_destination( response, diff --git a/shopfloor/tests/test_zone_picking_set_line_destination.py b/shopfloor/tests/test_zone_picking_set_line_destination.py index e6ee4a069c..50ffe6b68a 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination.py @@ -22,7 +22,7 @@ def test_set_destination_wrong_parameters(self): "move_line_id": 1234567890, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assert_response_start( @@ -46,7 +46,7 @@ def test_set_destination_location_confirm(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": quantity_done, - "confirmation": False, + "confirmation": None, }, ) # Check response @@ -58,7 +58,7 @@ def test_set_destination_location_confirm(self): message=self.service.msg_store.confirm_location_changed( move_line.location_dest_id, self.packing_location ), - confirmation_required=True, + confirmation_required=self.packing_location.barcode, qty_done=quantity_done, ) # Confirm the destination with a wrong destination (should not happen) @@ -68,7 +68,7 @@ def test_set_destination_location_confirm(self): "move_line_id": move_line.id, "barcode": self.customer_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": True, + "confirmation": self.packing_location.barcode, }, ) # Check response @@ -87,7 +87,7 @@ def test_set_destination_location_confirm(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": True, + "confirmation": self.packing_location.barcode, }, ) # Check response @@ -116,7 +116,7 @@ def test_set_destination_location_move_invalid_location(self): "move_line_id": move_line.id, "barcode": self.packing_sublocation_b.barcode, "quantity": quantity_done, - "confirmation": True, + "confirmation": self.packing_sublocation_b.barcode, }, ) # Check response @@ -156,7 +156,7 @@ def test_set_destination_location_no_other_move_line_full_qty(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertEqual(move_line.state, "done") @@ -205,7 +205,7 @@ def test_set_destination_location_no_other_move_line_partial_qty(self): "move_line_id": move_line.id, "barcode": barcode, "quantity": 6, - "confirmation": False, + "confirmation": None, }, ) self.assert_response_set_line_destination( @@ -251,7 +251,7 @@ def test_set_destination_location_several_move_line_full_qty(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, # 6 qty - "confirmation": False, + "confirmation": None, }, ) self.assertEqual(move_line.state, "done") @@ -313,7 +313,7 @@ def test_set_destination_location_several_move_line_partial_qty(self): "move_line_id": move_line.id, "barcode": barcode, "quantity": 4, # 4/6 qty - "confirmation": False, + "confirmation": None, }, ) self.assert_response_set_line_destination( @@ -344,7 +344,7 @@ def test_set_destination_location_zero_check(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertTrue(location_is_empty()) @@ -378,7 +378,7 @@ def test_set_destination_package_full_qty(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) # Check picking data @@ -432,7 +432,7 @@ def test_set_destination_package_partial_qty(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": 6, - "confirmation": False, + "confirmation": None, }, ) # Check picking data @@ -494,7 +494,7 @@ def test_set_destination_package_zero_check(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertTrue(location_is_empty()) @@ -521,7 +521,7 @@ def test_set_same_destination_package_multiple_moves(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertTrue(location_is_empty()) @@ -543,7 +543,7 @@ def test_set_same_destination_package_multiple_moves(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertEqual( @@ -566,7 +566,7 @@ def test_set_same_destination_package_multiple_moves(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) # We now have no error in the response @@ -603,7 +603,7 @@ def test_set_same_destination_package_different_picking_type(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assertEqual( @@ -659,7 +659,7 @@ def test_set_destination_package_error_concurent_work(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) # Check response @@ -693,7 +693,7 @@ def test_set_destination_location_error_concurent_work(self): "package_id": self.free_package.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) # Check response diff --git a/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py b/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py index 3166f9842c..ebc9f70c10 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination_package_not_allowed.py @@ -30,7 +30,7 @@ def test_set_destination_alternative_package_not_allowed_scan_package_whole_qty( "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) self.assert_response_set_line_destination( @@ -55,7 +55,7 @@ def test_set_destination_alternative_package_not_allowed_scan_package_partial_qt "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": 1.0, - "confirmation": False, + "confirmation": None, }, ) move_lines = self.service._find_location_move_lines() @@ -80,7 +80,7 @@ def test_set_destination_alternative_package_not_allowed_scan_location(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": False, + "confirmation": None, }, ) move_lines = self.service._find_location_move_lines() diff --git a/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py b/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py index 16c950ec6c..46e6ce4d7b 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py @@ -61,7 +61,6 @@ def test_set_destination_location_no_carrier(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": quantity_done, - "confirmation": True, }, ) self.assertEqual(move_line.qty_done, previous_qty_done) @@ -90,7 +89,7 @@ def test_set_destination_location_ok_carrier(self): "move_line_id": move_line.id, "barcode": self.packing_location.barcode, "quantity": move_line.product_uom_qty, - "confirmation": True, + "confirmation": self.packing_location.barcode, }, ) # Check response @@ -126,7 +125,6 @@ def test_set_destination_package_full_qty_no_carrier(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": quantity_done, - "confirmation": True, }, ) self.assert_response_set_line_destination( @@ -156,7 +154,6 @@ def test_set_destination_package_full_qty_ok_carrier_bad_package(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": quantity_done, - "confirmation": False, }, ) self.assert_response_set_line_destination( @@ -186,7 +183,6 @@ def test_set_destination_package_full_qty_ok_carrier_ok_package(self): "move_line_id": move_line.id, "barcode": self.free_package.name, "quantity": move_line.product_uom_qty, - "confirmation": True, }, ) # Check picking data diff --git a/shopfloor/tests/test_zone_picking_unload_all.py b/shopfloor/tests/test_zone_picking_unload_all.py index bf3a4f7890..c5e8b41b63 100644 --- a/shopfloor/tests/test_zone_picking_unload_all.py +++ b/shopfloor/tests/test_zone_picking_unload_all.py @@ -112,7 +112,7 @@ def test_set_destination_all_confirm_destination(self): packing_sublocation1, packing_sublocation2, ), - confirmation_required=True, + confirmation_required=packing_sublocation2.barcode, ) # set an allowed destination location (inside the picking type default # destination location) for all lines in the buffer with an expected one diff --git a/shopfloor/tests/test_zone_picking_unload_set_destination.py b/shopfloor/tests/test_zone_picking_unload_set_destination.py index ecb9120c3d..d3320cc591 100644 --- a/shopfloor/tests/test_zone_picking_unload_set_destination.py +++ b/shopfloor/tests/test_zone_picking_unload_set_destination.py @@ -164,7 +164,7 @@ def test_unload_set_destination_unload_package_enabled(self): params={ "package_id": self.free_package.id, "barcode": packing_sublocation2.barcode, - "confirmation": True, + "confirmation": packing_sublocation2.barcode, }, ) # Response has already been tested in the test above @@ -219,7 +219,7 @@ def test_unload_set_destination_confirm_location(self): message=self.service.msg_store.confirm_location_changed( packing_sublocation1, packing_sublocation2 ), - confirmation_required=True, + confirmation_required=packing_sublocation2.barcode, ) def test_unload_set_destination_ok_buffer_empty(self): @@ -248,7 +248,7 @@ def test_unload_set_destination_ok_buffer_empty(self): params={ "package_id": self.free_package.id, "barcode": packing_sublocation.barcode, - "confirmation": True, + "confirmation": packing_sublocation.barcode, }, ) # check data @@ -345,7 +345,7 @@ def test_unload_set_destination_partially_available_backorder(self): params={ "package_id": self.free_package.id, "barcode": packing_sublocation.barcode, - "confirmation": True, + "confirmation": packing_sublocation.barcode, }, ) # check data From 6d53ce13a4c634830e924b903180214ec64347a2 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 8 Aug 2023 11:46:57 +0200 Subject: [PATCH 02/11] shopfloor: imp single_pack_transfer confirm --- .../single_pack_transfer_diag_seq.plantuml | 8 ++--- .../docs/single_pack_transfer_diag_seq.png | Bin 29489 -> 33901 bytes shopfloor/services/single_pack_transfer.py | 28 ++++++++++-------- shopfloor/tests/test_single_pack_transfer.py | 12 ++++---- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/shopfloor/docs/single_pack_transfer_diag_seq.plantuml b/shopfloor/docs/single_pack_transfer_diag_seq.plantuml index 35865182d4..3d1f92c834 100644 --- a/shopfloor/docs/single_pack_transfer_diag_seq.plantuml +++ b/shopfloor/docs/single_pack_transfer_diag_seq.plantuml @@ -23,14 +23,14 @@ header title Single Pack Transfer scenario == /start == -start -> scan_location: **/start**(barcode[pack|location], confirmation=False) -start -> start: **/start**(barcode[pack|location], confirmation=False) +start -> scan_location: **/start**(barcode[pack|location], confirmation=None) +start -> start: **/start**(barcode[pack|location], confirmation=None) == /cancel == scan_location -> start: **/cancel**(package_level_id) == /validate == -scan_location -> scan_location: **/validate**(package_level_id, location_barcode, confirmation=False) -scan_location -> start: **/validate**(package_level_id, location_barcode, confirmation=False) +scan_location -> scan_location: **/validate**(package_level_id, location_barcode, confirmation=None) +scan_location -> start: **/validate**(package_level_id, location_barcode, confirmation=None) @enduml diff --git a/shopfloor/docs/single_pack_transfer_diag_seq.png b/shopfloor/docs/single_pack_transfer_diag_seq.png index 60b3c9202447d3b697b4f86942d3fd41c6002f3a..cfa643e3da4b35f6b40d33de48590b7aa37cb4ee 100644 GIT binary patch literal 33901 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 JuHSw1{{X;ew449{ literal 29489 zcmdpeby$?!{_h|P3aHy66vUuYh6VwPE~Pu9B&53yL}f7O29>U%yCjAN=?+P0>1OVC zaG&!#=bm%#ANS9@pZ)mkJx;9mU2A>6pIYy`$8zFliLMeM5Qwvq5|0!R2>e_G0&nm1 z33x>#50#HV*zQO^y07FsxHv-SeA(fs-iD-;-yK@D8y$W5~8+Qi&0};Rhf80Y7z@HKr+zXB5 z@W%}bd<0@VQ_yKSXe7ZUZC8Rbz>oxi@J@aB)N)QcVv!l2DVXWb9qZrY-`UH4SJ(NG zSq&FmxOB;Gv7cj?fu7#vN8Ej+(D7F(i|go&x~-IN&x&Ao6GC;~qfkm~md?)3XIZT( z4;`CBSc8AweE6npYtYucRl7tf(3^I%w``$ef7yJvD4S#$ z#(=bK_;k~cl$D8vZ>(LMsvXoh)S8 z)4KO5ib)ZbkESy#Iy*Z90s=Ih8okT%r$dBW9Xd~)I>l=>8srsxBHxOF26vyUf)tbF zo)6?qC&hX)v9YOOf=Gj+#=-@5WaA>=aY z!es?DwP(hj!7Z+fJF7c8JJZdf?U5q7>lzeSOP=p<5mQi*)~VAPrm^cDt=3b2Gk8i$ zKxj@xqLcTmbueFqmVS0-W^kM1DvbfmEYq^wK=jLnUBb|%xATJRU;CmOLQhQTwlc3( zVvl%i^T(<$nFmVyNqKVbzfL#oisZGj94dJ3sTb#Xqt`1#EqlVyER|nfC6V7{&G4C8 z*xZTfeAD%r1zyX3X6iuP9Zhv+5iXxj4tUqb?Dq1{7Y?O~G#)B>b8V82Ja-eum-J-D9I*(%=KW+ieFj>4yUAH5^@JB9}=?Gu2Cb zzn)GjJ3+?6=eCvpT`ejk#8~vjnntB(`OMb~$4mPtEQT}Y4%(i)eIp)L`B8KM4g|2(RD%3;gA!zRWq0v8E@j|<(}$StP=sgO?UIU!BM_m zSO4$Vk_!d$TDd=4Km9s-k#=OdW9ug^63wCJY%hcQ?tC@R`sdd=zi(f^8Sq-D6?z`7 zO*fMf5E8Pf>5b>SS5C_#zUy^VmVK#3p?d8Kb1zz#-6-?@dm^1YiIAt(6IKQWlDG}H z^NLgR<9z+FGB)bkcJKS#GxSn^*x!@wE|pW)-wl@QLAQ~%mLVGf4 z|84n=K!$&+)#-GBGlQJW7kS*>4dlyVX&vCF%szFsiNGfilqHInx%X5yjm z`A7z>oRz}t?1;d6WR}p)`|G9$iL!frauRJkhk~~W&t2yt|B!JsCycxI_mu6P?Xn&E zF?8}oI3Sg6vyK{Pu2HCRU9uQ1dLmL6BjnT;RUNpsq)B=)XJY|<$#L-qs-?BHN4_vk z70r`W)s-Z_pzB`te2C|GCOyA-b^NZVVx4KfF6bWKwpdy@nCz~krDgH^i)lf($3;z9%DCxNwAQuFHFrNxY5x6w(ZeoGleqYY>phKe z%?}<}q-Sld3^Ae=O-)IIvtTR^QWCPVw75l}y| zIm5?|O$FU5ScUiFqJAu_Q@?-K>{a@U<4S4rvgZzs7=5>nP~daZnbZ?8=M5@CLIyfH>HJd8dsHSUB4nngd@B7EL+KCVF5u*?eU z8IA1f?8;y;>6oY!nErIU14}m_h787_*r|oliV|x9Cl3u-T)HuXboBHKK`sZiXPBEh zyzZX*Gln599xeLrnZp2c)0-*Tgbqaf6|PvcqX2iBtZkOu3z4IpZwWrXbWLP%*IR{l zo3-pMcUdld7($x!yc)*RP3X1H`Z260&EZ6{R6==a1$7k_m5|rAudvS~qWqoQna_^alr1W{TTun}zn?Wwv` zyld#_eWUGk|Gh5pt#b_ryD#N_rhWao5%}w6TwGABSB1lZioX7r+TNU;9B4%o6B7pq z2aAh~MgG@h@2)Nl7uylYA6oWj-DDQjl=@^F&&8-H6;kBBKGPbzaeZ6t_OQ?gDUM~fwyK)x?thgUP2X-Q=*h*z_;WpfJDyHQX>}H@xSZuNan-;lmfsGe#>cas4}~X5=1{zPVC~j1Q1Qz7 zVE7A56T^>A4xOcUuh{AnbdtI3Ip3X{T5IKX3k--?JFZSmPVSpWD`M`sgI4;r(wO#3+tG!~`E?@h7Xux*Y7PRL zo0_&32XbL`b&;vEoF>|H9mx3=7RG7P7Nj||yFROmR=?d9$>+sF&8k_z;@OqB(1&hj z5pdec&@TLd+v+PSUB>E@d18n6BW-29UUrzoTp9Q}UP z!b~f{FZDvfwt;u-wU5v9p#oWt-5leIYrK{XL2D^x4{E9b3LP(rd;IU${XF?oDzfO8 z77He4FUo1{&872;oOtT_s3Kk^DIqDA&jtqw;(s!$q6gJxV4K8KGA`9N1}4eJwzakK zP##K48x<$XMFZX`u)L2$APDc{t30k$|2I^M_t^S!7Mm4$9`3=#;{afu zX_tV*d=}$R(y^8}Tv777bkvz{e+}+D4 zZ%AT3pg(>9xa#KOBDC49SmS&CJTY;p$3Bn0g!{2mtp+@}9b-p+e1kGow(%ePrgw>urGBpHY&S zt6dYIZg>C}?VGic-M-*8;loI@hxwug(jr(YkTW%3}s& zLjZai8$&7iVmI5ir|5aGTF-^_)fX(8jd8QrK&PjpjPdRv#d@ZurxWb@hblZuv)>UD z6RT!uY%b=u2cV+A2w!{*#mPR=eJ0%GnPC7&1|dwf+BcO94FO-FG-^EKwhP_;T$KlF z&8oT{o5ObPF0QV8ahy*)uL5nHor^rVFWZJOva#`GKTgTmm?4-AZu1z( zG1`*F*giU_#O_xTUU#l~^f*}+9V2sF{1hQg@z<}nq8X>mM8UPw>+UfkK$MeVWiB-*@mX|&t%AmZR)ygy{M zee;GE`%bkbA?%?&vihNil@G4f&$H!CII4CHH6IX!)?x?E(UO;WJiO}C&=pLIw)Y4d z$sOaoqHUH2^TqwI<{LGK3=eA&(o9lt8iy?h)%#yFv3MsP!qQQBTA@Bv*V7bj$l_6s zMx(>)11gU;(JEt=R!91`xw(6y>@YJ)Q7~^SRWI-{q)vMqbM@3-!zC8_t|t!FSn8|a zzCFEf@8u|k85kH~)yVtdRb4m|IjEz1DOhTHdD&7MlW)mZ>Movc?$TvpN>sXtz0${5 zVE^SxUTH{ad5gtwSg4Ul?Xq$#q!h#U9Lx0L@&_&@7yp zo>qQ@Q!b@3Msu6+Gbzc>YHoeMicKPyU8lkeh0*f3` zKhinfhwyo_a|<^pYE=07`8y-S#!AaKdo??Qhd3ypV%(ymn}MF;;^NZL(Xl7e(3fwn z8g0vCIh^cuLBiHHr_B&4h8c=M;rI*MqRZ=TJ=AswPtyD6JXMB2nQYHeG=E+J9DyMP ze(4%*tK+tmZ>xDvCR(7U|N7>N7j`I0toLIbY`T7+hS8-W`?QmwVt8zHhzHyxA|e7z zeVv*byIqMLsgQkMkA8P_m`{MN3##NSv=~^cCT8#Nbag!Vpw_pRIM9EYXBm5CgimGh z+LiLsh*6#CWu-G`&g^+fPzt#!nII_?3oW&y8I5%QTrM;&X4TAY5n^*xY;0_KK(_q^ z8JpIR7%{W2oU&bsvJOoY{I>CJTo})z=P;QC^pmTT^$RjL>aJ=HpBFiZ3ofH=;?){uFd1?-UKA%dZC8tSZYHco71ETx(NUkl=_r`Y!|NCl?nj zE_Ds1r7Z1mU%&}hz3JinJ)b@q{{G1P=e|m7j>`P!Ac>ZKo)zw*f^oIU#R_M1?RTdr z*0VMCmv|~-qq4J;*cW1-WZj~;w$E`P?IZ1#&r>8sr%z`|M>Fq5+4#IMaHBQo(r?f? zUZ4*G6b%ihpk!Gc&T@v?&1Hm3XHPoSc82iALXVp~(*5FZFjDd4OR0JBke?j}e$vHZ zWmFsc`P|+H+SAU-30-S=Fcm02FQaF{3OvHV4e2?blBC5&u`oQ(1_gx-wAZ;#+E#qA zrlzL!qRzps1=N`90_)*tz;5<~hKpf|1#ZY*y44BjY*0s!b|yb=dOiF;KFQdk^`5Ho zw&O5%sRy^=KUhWC(%T&@n{}|v=$f!hIwS`5&NQ40;IKG*)?;sDAkQ>#;}wQZv&23{ zZlwzlg_I2Do0vb6^GP|N9oW@2lbAV~$6wk53k{GzyuRihXmHvt*~i|Zdol&6V3f4 z68YfBn?tP|%0fKnbm>Y(^JiQKrRQ>XbCaIX$~OzPf2*3Q#mLH9ZZjzckj$QI&&ia8 z0vUfXFnh{5`pk9b@fUe{T+xk-n|BQk3AGx=F4oo6CALd6#$IZ_yDl?LhkxknVQgsl zO#X)EZY=h|q*$73=WZXgm(KAAc6N5UUWX2LDyhKV$J>o4_VrJg$YHk3%mh7UabhvF z?W?8S>ws|f3ggtZDFi*mnQl=G9aqtRxv9V(`@+Wo90rQ$ZD8X+uL>xx-34H-Kl?*3 z@$5AorKiY{+ryI|MKU@4LqkI;rMu1YDQ)9J3s5x0Z0X?qFrL&F??U4xKXV#Sio_S!Ph0l&e5^p;D!Do8|C5G4X;{h`;}249v&V} zPDSK$PnzoX@`#{*^~q^pf)=fmuV25$2)WIB37B@$^hseHEzI0JWP?5f%yEWR+n26> z+e<*F(o?%2=wM{O1}gTFl^5qB`AO-a?>b|j-%DT$cww7YzPgq)+=%Ou0?hzw<2Yp} zG5)V!y<*ocfi}6ZevHE?ae#rc_dZgLmd0T2VaCGZ;?7vD+tyF^svw@P?4&WXbP&3r4?*52MHLNv%hDM&{#l%28^iE8EbJaE<=JRXEJ=6D`43O%++95MYz0k^v;d z*g&v)(JHW+>XA{sT<^v)Z>8;F6z$43@AEN~r~1 zUtYd_Yia2G^}Bq0eEl2iL(B__`_M^RV}wfKGYUNbIk-(bAH4y}n_?d{H#fIO|Jq`4 zV0~?^x~2v&`eqAfI~UkE;H-e9vt|WrWGLUf50(Y*sB_KOm>0Z2AfGA7N8wjpU5&0h zDuK&bQ+;irvb!zswHHV^&duAP`Pno6l-LK#fiDjUO|1vyB)+y!fCC4`;un z_F8)MDas3Rg9k?Kb-*DgSPCE(?lA=g1++;@9*eM}9#gnujqUA8bH@gq@*Rpuv|O}+ zpyOiP$nh&Z0u;O!{nh{mxUS+}__jwkxxZ5B1xpREXdYIPmDYBqGya_%BQ?PWTsZ+wCT2$diqhFxfd*p33%g_zUMi&=Ax<^>%!Cz78TB^cFjACx)D?L_GZQVn+~_w)^8Wk#~B9rnHlE@~rE<&YuYd<-Yl zAJQpXZkMpLea<))_cB@D#C_f7FZF_#Iu{f}wVzxm8Y+NbLhPH(ubAZVq5GE`W%|yN@lds<2)4~f=mtrV@ON%m{jsu;8i50 zrKhJB@zhFCkldCUBdQXc{Sbf2k7_+aXwmPr?9i_&ydpE?Xu0tBGyA>2awEXAAkRzjf^MgWn0oq9aCluBtA; zW?b{A%`IVYsH>*BnubzbObk<32P85@iRnoCoVVz?!>rg(8vJBRCs`8l3CEu{_NQ1` zAGJ>hgHPE!2wsQ+$NC4j~CX?Z-vYxZFHWwC14G!!>uA-#2z{t=38^F3*_l+yKAuV1eK$XZ+bC|Ig)=HxwO zLEkMV%VOzDlCSuL@PBqlQut9IkHCf{B_t%|%5{ zdsn0|cG&glBYzQ{<`M-3B`xRD#}11vgZw`uJ}6hduA4jo7l++yJ^1>vdXYF>Qx)T} zIXEjYvKnB6H{V6!)YK#s^bkof4uvEA+kS?;n^jvj_4!%tN@WcNCXc~4apdd!CEmP= zlmiZriCBsp_N%rfjgOe}<>ZT=Ek=EN=WD55sji~ox-i&!yY$?tN8HAc7MxC2OpCbvBKr~;5+x<9znK1NqeDHl`N>i9Y#E2>-gLKz zxY0|hCz|#GU9nD-YFkg@W~vO@28aVVaGMSzARy_xTkH~WP(iFCw9?8R`XScI!s-34 z$H>vnh(b_MxF>`1s3q~oLN1Etm+>b~xfCYU1QDg5eaq0m-cV{|-qAl$pxQn^tx@t4 z?w>L}1=*E@t-D)q8Jp90utmGKlbTPevd{i%xmbE5B8?HjCy(Um?VysqYbf`XwGgez z!J#&4tlN;sML~VInnhxzO_8*_;rqiMDaK^%wwbPznZPW8BzWu)u)2H6B=}4lrSO$X z!;yn0U9uY)y5+*A^{FelZMtl1Oua!`DjJEL(m#ec!3vF{r?|H83+^K0Z8HDpZ=B`f z9wRS1ID-oDYLc3Us)u|+=qAe+3YS&%3bsVgA!mB?Bmey>r0boWhvcQ$8Q&AVm(e=) z8E6%XKYP2Ob9LSL{NV!~fi4CshfLAi6!^)*xJeG=Vze?jADMpqOETrmA6aEFdsjaF zEafQdKG&1$SR9gh0~S_~BqpM->7Ae4Xt~;w>z1;?q2Bk5p+S=iAt>UkJb)$m*6z3) zbm%#+v>l&*#Ut9GvHwT$&;Sf^Gjb~c66|I7h4rEkkS!-}viLxUc~4*=V3nhM!NX{$Z!MFP zt2AQwp?JM zb~M(gMLyE8{jfchgH!eTfI~x+VY=?QMsn}lf!Wy~4jY4iqr*t}c!6ghuk zt>fi-eY;drI-~wk@*LE4E$aox`<^d0>Y7qQK?C>XhjsTs216$x(D=49Lz01onYC5G zi+^hw|5e7Xg>-3*)TtnP($L_LkrL1KXA?JZ|_K0a)6o zBzX@t?LWA;Uf&Yk$$2b`zIy3WE|ho>={6Vo=;`QEm6&+w5z@iIAsJzLZ(HWyVpsR= zj|U)!nP{k}Sr?N{+hB88*6g}fai(io`S$JG2?oJllM@pGfq^|{+>w{Oudo?wk;GCw zu+8}4#}K5u9S2vq*2qUsPe1)w%DIX+*H^i%adH3~US@&0HLYX3q@%p=HSvL3gl*H% z?nKthjq9Dh7Havzzstkp)k;k!PEA&J?yd;q?q?JVSd=nT@PTY4qi5GzoY8m18P+KB zzcyB*d}2?Y^b#nX(2-7L`E-x6z`1T-q;n$`6eZOA_k(3Fx!tm641azgF*NXq=Ben; z1jT(~`qs01@zrj{Oa~fVHlv;FuR_}QHTK*7f?rdP{wGD zM~_~UNb?pBJ~33!d8D4nqCJ13Mu9pd_WK z%`-&MHgzVPQ@G@+jBq?(AKc%ptPgDmut=}41r0sD)kuj0VD~0nY-ha>|9ci`exKXY zp#l=Jzm)O%g(y{@va9;QR3%{=9Ho#w9-M-|Me#{ zkSgb{fdyceuW{5Hn#sz@D7ja3GM6;CkV37`CXiktmHSXxkW ztzAHjiduZ>xHMQ-fR9L$!wj>WsG62&O8@yXQ6{|l&ue$}PJ8>&O#&eHn79S>7g`Pu zzA7mpLuy@GMJ1G>M&Xcbz^Yf^YL|oAWQ)9ryO_sRf!VRKx>{OV z4<0-KIrjA=WDj^~5qzr1nR9nxF!eN(M~D03qM~&ej1-@oq~sF-Hc|-dOfWBx{}$kj z&zm<=d>{P$K(Yd?4Sx%vw0jOR7*9{n02JV?-gNaqFri{%+7eG54^a<^6Y)QO{P->^ z^{#gRe3*INB3w6o%==rQ4dekpRVSo&KOTK{T@n8Id&*LnivrW%z}ss04NUwum?zSY zGXaYAWoq|jXeKr0UyYzRJUpDvh>VO34ek5!MqT|GB~*UGb9Xq!AT8peDj z24#*wpzegBd`T!ezlstY6@e3hR0p*R(qxm9lR(N%C*oJYvnsI~t7vIy@gXK_O1dqE z`Vr#{CctcaY~gCuDu_xVd%wOx#0Y{Rlm1t^QvaISGkRW#&iIe*rsLODQZY+^S7UNpUQWcnakF=FOV`^kCP5LTadZ z;@Ua*xTDX zI&ML%F`qum06AkugvgpRBl59Di)m>MM-csW$D#Ks!kQ6glprv>uy7mhFSphH#$2w) z{#JTA%dTpo*VJe6pjH2Cydj0Y=c#*T2GZ3zrw#1@V1p1LLvL_6&?;g=dHFIIXljjt z^v#u^dBA0QKhpAxm3?9ZmzpYu4DnPD193rx(|B{fCtAQE6(o?-(ozzJ^XY%#;o)7s zetqyWudwhDSS9J&r3HrOyWRH4-dNnko|DBub%DBf2I2ks^_Pdf^T&F-^a-%v9S4cO z(NeGjvu}&y2QyP6OYi=pM<-zm&`fAVp?b{nr4pnF5%EbMsRUI-DE4=DBsT14Gzk)1 z;pPNdml-w&G&D3oLLi$F@7}Gg|3L99d^J!}d^dV}RE{Bc~e5)v(-TRn)bnTQijr2mvE<&!WtEu4HB@taxxIX+@N zAwHf4EH>Dz6C`%73X&S5W(jzssXGl(j$ahnUm9m)24OFE1?YE^HycnUcmU zrKYdV+Rtct-UdoYNv*A}ro36w8)4d>?=hcg3EyA>-fpuS_1pM!S=rt5=Ra<2*n>sS z?K$i1V}??t+yUeKSa@X-<=n-oS*dmE6f`8&sOq3qIxtPfqous-ktk z+}g;H>x3Gw#SLuYN)KAs3%p=}c(7$IT(|%n&4!k(?sH>5UcN@L?KBvrwk3ia81%-&H;gYWb%WR# z%pX@*dn9lB(2!kM35;FF)>)k+ds6Car}6UK9%6QDAnQ1)di;TjtRAYu=oNeI64UYH}wympRaN&48vFY-$XR;zs5Y)u z4p$q!q?HcIGwVsU94Uc9Je+M%&+^`q^!d40&7CQ^j;;BHrDfV>&e@C7P$NAj-qC~U z#5(t*zFwS>2~6tGtxrw+xwEa9@j*&n`SIlybaL{G7oWPaUjd}^@$TAb59Z@-$Kx;o zb^6moH1gng&H3gRE-*&8pK>xX-4zezu)l;I=DWci00Wanw?a!eI4oIc^_o=~==Wz0 za;4XBO6!MnLarM{_Ms#);Yd*mY`3EPQh)YCrrV8M%H*T~mfK%oTJ; z`r?CXA7ZPD-Nd=_jlR;p<>tQk6~VaLGie)Eq#mE1-qPGGeM$E!kA+&P;}SmMxnEin zrh8`ZJ3AG2^B`R?cuBXyeV}#`Iwg?7i#fqRC+k06ySo`28M&~ulo5Ue@?*ru!Gh;$ zUMJ)gDiUROl@t?6o_*|7YU%6S263^;8k+hH`qbfp;S=Svy|P4S&Yc5SN*xf#ck4uo zI|ZZ!e%u{e1ffsix^IA5PZ81@A6VEVi-A>Q(2NX0!5Z1Jq+5^3bQI7%%o zt_;!kmoHy}`<|D?RS+70qCyG&MpXF(W}g9=X-k7kfd?1DG?inI_Kk2|6T22#3H6cO zuV85jDGvlIo}oXqm8dDf9tb=`<0%_<=e}22n>WAt7yYlrBy4qRKQW2C zoD`aQ{HQCIEEMKePMiyKVa6QMs7>(@%{1_1fy$s^6$%RrgImW_9(v@uZALVZ@(UD6 z8iNJssco>K)6-=*6SJ}&e;9zAaOk8Z^f8p^5J;NMVek%8GP1D}2kBYYR-l)F=$<`G z-Z~kPYmbcc@Z@D-Sp>le+{HC?_O)&r0X3(#ot@Rmh_7IzB`Ksd`E!S;^yrODbwP?6 z3@%47+!F1+War;eOV1WyP@SGP9C-^hTHoV}@0V8L%YCDA$r5E2WTy{M?@8k+J8?F7RlB#Wk&269cf z6b zLzeUExLHyZR!-}=Xm8vSrhn_iRZZeobyw3~fu)`vdZYkTsT^oOQECGX+=3rrK9i!G%ghZ>v3<&H*j#ktu+i%DUyjP z&=Y_d8QpWy^HGD+R(pD>Au=osXIubc)y&CvHxr;(Xpf1FWw+sHWtHXhTdVSgVB7}^ zPBFXS+-~t415l?88DSw}E-KjAtio9g+Vti{K!A#3q?a#C7^=qHxq%;nr4FqY|9G8M zv1!6SM)s!P@6F8&I9vgWI5#hb!i~o#T$x*qw>2p|Jp7~3RzEHb#PW|zt^rf#8rL&D z&!5J0ZP06e4M&;O%~;<4_&ZIdBlqE{}@-aO4CO=PN5&- z2h*-CQWXBU!u=rBX_%7wn}Esh%Sb>N-b`VrpB6uZf1KfymQ@w+)4lS= zizxgLU3=_bZ4>?b$JH;*(>;6wYu}ZB4kmq?Q^LbliOyo2dr8Kgrn~(HT|{s1quP=h z9UcnAX}r2I3>aEfjwPpH+I^)*ty#fREbt2oqjG%iH`bT&?*Wa2ui!vbb*e3r7Cg|& zuU2&&F0<)M@Z%x|&WIN`ST|8};mh`GwmxA=l4DAg?^n0^ty;IAvR(4&@e7*{93t#| zn>6(A@Z#Rn8(5O!{lQGwH-sd3f4Jv=qNab4%51iOz8@dp6W~Z(q;&D%;P4P!m&37{ zW&zT#(h5flL+T?li@huge8gXQYXNy$D?5xo6o~0F-R1#k_tb*YEcC z^%V|F7&d>`F~RNL3Xgq(hYTq}U4`f5Fl|~99Lvkga0Wp6Hc{WsH=3)~%O@GRVx@1U@8%V4Mo*BJP_#s_>aF@R=fO zBtVT^N)$M93^lOY-G2Iz(J=JEb(yrsXck!P_UurgA?(+z$;(?vCmVsHTk|;nE-->wP3Q*Qtj>R zFo-53CJN-Jvz#rZ+j?%K2X}``!2Xj_<3+GoWmXby2?-s37Qg<*3U}P2u|HAH9qdlq zU&?QA!A|}=Rxn0D6VN{gof_t-M_~S40m(v}1%BL|bn#L?z2~GqFW?q^3rERO2-ggD ze1Q@ax*YI=Q8AHWGS+oI1-OocQGBw~n1^a=YWUG~s4n;+O+#~CNltT}qF=WF=Z$({ zJ&fXCb283PXaoH@obY20S`DO^R|!c#^(k4v3Ad3_ClSy=F0aAKH804(V4(lts#!t{ z^gCBqS1I!aafVj!<2Lnf21(wHy25s8W1&wTEUVuE82~fI{f=8!e@>ba!P-j*p;E6z6?%$tLnM-!@BWc{<1cZ}<}9}jx$ zYIGEW_UdhD;X$vXq5_G&#sAnSrL?5dJzFv)9zxy1BL0E8}%^;L<7%l(I+U1wEs#VxoI0qas0Gy*bm&!h+j; zH=_|BGqd!1-%C%=oa9u31v;OTgoKc?W#Q)^(BtbOA;+zih-YlCq&ZZIWIlz5r;s!o zcQ$HH(;4{j_+}eXNMqo7-FO3CWK~PQM%Hx~mX|G{&i1B4Xb+kSxU7SUSBIn}aRw3u z1e$AGZ~gxPnV8FQHN_3dqYo#;baZrpu|#k{@v&`zR6_}+QzW;!G0R|q<%k9bj%8(o z=?oA`pwB!c5_NcR@ZjOYeEJ_z34wS^>!l#aKpSUeWexW7ABm&u_+Da%!KHRC!x>4z zU@2wG^z1y`)WpI8oNSm%*zG*g#Qr!C@85I3m`T-(kxuTzU;qxdhtL_UGbg%ecCA7sYJhT>i&)wY}s26O>%F%NCm0(q!FIK0Zb!7Ab>4QMA*(hiMSlk&&CZ+(d>C>zi z#3@!FU1tTy-O|E4fxht%;7+apor;jd?BxQK-Ja_b7Zli798fHFRwdw4b(FgC*55xk zi*5ZJ`JE~?*WcUQE8uv`eDly>Fp2d@EY`?_sub&WRE+e5f)-)acrxPa34HwNC6H*0 zHa}h$5_tCGhJj1N*VL=&pe<=+gv4^cf-|WXPxYetz_FQ4IRp#<9FsF~}SUp5H26dyPl&;{aQ9S=e>s zS-1je(z*qa`fD`W3gm&oLMwDJM_Yd`V@o=)GgIN#mKOKL?D~p>HEDx?Uh+&^^ibS= z{B>epPMhU{T=ChNyUaYk_yO4l5p|u0TFVtwR8*{&x}n|!w1>KVr#v)9z~Q~_9xfK~ z{GT|B~nyR*U46;cNRT+q%fNDX-XcJ}rv;Iyy> z%Jx;<_oPvk8pFi~U0;GI3a4>AZ(@3b>vyJ@a3HAYY$hZm=4~5>V}Uy zBOG;$Le-L94oQF$Rr#cmnZuEyAn2r$gRq3N{)5X>j}gQtI7b0M;s%_MfH=fGB)EHU z-0e&t2mIE(V8Bf1OY-=4F{j^TK+{U&3F zZSp}~CKIrF_;;M9(Rq9CWUJ&vV%oPO%E~WtkCIQS5Y|2gH#pVz58+!aS!a5e6ubjh z3&Uj$$mX+M(rBkH7`|5VAW$U|{m!yPKwuMdmItRar9MHb1PwDr+HB0um_J=UbI5wj z4U>M05ax*B0j^Knw{gBfN=$J5%3@FL7Z0 zJ+(@-clItK`KhznHH6YM@ajLJ|KEN90*C36RgUrG|II?MIL+&a_yhX9YVhCu{po*- zWdQrwo^uFDEL5* z{4)$mE)<1VMH`SE-)OGknJ=kU{xU+%tb?M!2&q44$b$xzRw1NB=-C>p*3x15j^ z=PCI>|M{bj!Ze#jqjGa{hDSz3cE<5RPCZ_wsSe1HdyLdLfySj2&Y+vbxQON0g5-gZ zkIx;ryT9LT2#FVH0l36-L&J}@obTVigIpx>XQx*hHIue#5lELphKvNK;zJBy7qoN$ zkgNu9bH?8qYT+ZSL%$o|HEj4q%BsOogA1U^^a6N+Fj2IyhiiB8lZZy)!;LOb!HtZK zA^-n4k%HstYsV0e>0nhQrXDEdhC3bN)ZE${fD%#EkkkTD3jW1IiWn$KJ&n%eRWESI zLRLo0Y0Y@ex|0F%Sv*7Zd^+|Kl-#7={%=MCa8Bd^&}rGr!GVFdyC5}aZZZI(R)aMp zA8mC8>`gC58y$RqMQEh9mPb-(-q#jlhk?floQEJgF)>W0;`>wHW(jd|xYK6^bdwPN zrl>USqMKfVG%K?eCnu)^9oz&>SZNT$Vo6awgO1oq3pC0fyULsaUXwtQ00?=aP(;!s z`|5ixBlH$}E`JmMxV5oSW;^|UF1HL&anLI0p>Se^j9sU@4NVu2o!uF<+GJ`>r6r4p zxZ=j`^PrgXHXEDeS%i|vzgr6VXcA#?V1}$JGf-9#TedahzkS;P?z1tSfk6V~)*oW9 z3%XbVkG;Ih3_XFS7X1I=DcIpBTY{{AVDTQJB6x}>*^-DNEo6w!lVfMWZ3+&l$GXgf zgTIsor=^HVS$=5QuR_!qj#g;~K{f+dK}&&9S#EBw_TB^pb3m1f09pao6OFt0qeofAcuKq%ZCK?n z;AJK#B&d3S+%Jzjhmf;?QvrL8{h)I`O-LJ4VMxoe7(kw>hs5YR6l%EO`L)Pv_Z)6t z2*q^{!GlimD-xIka2|kMdIL2gK+sELO8e-4Ou&AEU4yH%e;W=6>u5uq$||4wmpaw) zcEPKPFw~-jxBzb2N&8_52aT2Q&P1Xs_U1tw-cvpk>$#UgGYLDosHiADKHipE`y}Xt z@9U_X5n3R+OMyQKrfq!Vog}V4uMeVK&F>rL8>AXjQ+mvJz;*nyv9%-smxHJ`)lgK# zH+vA?wN2n(uwnAV$iXndu*Vi`hT7wbm7CmSCi&@)U(>eBnPjm4A?ttMW@k6>_uC{K zTN)cb{{<&Dqt;F2M3%QR6GZ%&(rz=;rm2AIK=-E@T>#$}N7$hi{J$62$<)zz`}_Nl z5FU!rOnCDRjV)NYn87T|2@tT{xGkzN^wRMi?!Th3#Ad|z56&EIXkl!M5$-|B%gS!T z9)RPJWAeD0tjt3(pJJt^j)gOy2XpdXgK$d5nC(@IO*ku?&S5;)=j>36R0*`O^3e3gs2Yx*<4 zu0tIYE9{~1hE~4$H!f2ji(2T-NDD;KMlja^cj0xO;N<1)1MWZpk26_<8nKaUU~O#; zVlw1m&SpkCo&kH^8blr>i(l`*%l9ipU8X(bS_&0;hLUhKPV%-enS03%z8P9<9`nhIk#Mj~{O{fZ2ej?!hAO?hfL7eSdzWUD>8J7~FIb;#ng$6y z@B|qQHB6DTm*bgUDc4^2ct$A#8eZla!gG|raL01qoDFt?7-Y(s-aWsNGc+r5ptHNh zzq<%_6}NP^aCnBci>zXpTpH$hZ^_7@NFIrcgFe*1Q47su$)PWE7wv>j{6spwe@5vs z20u-5xlXs1kwH;crckUCLXjB0hVz z=Saftx)K4`EXwG{zi8{mj?~oDuVzu~I%WMJql3C_E_Rjqdq4jqa2H6J+HJpDn;8T} z0CZ}0e4G;o`#@ISqKNoQBW0xviiphMaC9m6s_hsy&SI4kNPOZ)F^1%Tt>ud^ltQG zSa|p@Y;}*_=}c&ZY(fu>^>59iu9O@_vS>tti-^9d;(YD7rowL+gL3iHcoh}Mu;Su z^_c!xAbX=|D(LN9{7cbd9iMBT0w+>J8Of^LPno!R60WIX`pgp1f8O9NJmjcjmm1{U*^hd<4sqlIcl=b&p}XOV#7X=p2Vx zWo&;zzP{3wE-z+;?1C5&r?g|@M1M@MG=4Huza_a1oD0V^Opz+jGN1F=qhe>01wAjU=f;}KwK|In(|ziAcVt{;%< zzl>Jq%xQxENuDxnX}tyGZwo}hLA?-ox&kCAVq!8?(jW;qKnp{r@&$1pGTie2g17ZwvAj`E@=rrt^Da5xB&V5x@8W7ikO# zP-43BKX3B}eFYR@K1pPOm3Oa}?9E0#78VO=?LY>gr7q$kU>|C52#=P+#K@?)v=pba z7SJ1%%1HjX-FQ`S?&r1W?k~t4rGZogpH);;3|@r;Zb5~}HL#r5ddJ3e>EPl()xIiV z|4d9wtaug$6bxuh2Y`+|^FB*hM*^`|Ssu@l2?Q|$oMI9c`cJ;47mMRtskE;eouM;> zM*o<=Fao*)(`1rN_$@2iTqY}#f1_HMkK`QH_DG#9y%!{4esgZJQT&j^%v?@Befsn# z4oEWyfXE6DJ7A~*kt~-1$0>h*`zSm=Qd(HMAm)D+nvGvHh#DrA$O-~P&)S8vdyTkb z)DQ|Wz@R?=1WT-eD}XBa<8`-14j1tIbfUqqwlyq+;MSZWWJH*2;HfxGniO{&!(YEU zaRrVnC8PMAf5(xeDKSw%GA%k`78S~_d;Q|Yi%@yV$dWFfLMW^K$#tq7npC7fxp@cB?LBnF^4l@=E>HYyXy&`*Lk)@W_~zuNoqf2iBP z?IG3GLZvJtRI=-m6he}eHNqIiUWDxX5-ROvsF(>MvS!AnDa%$GURKutiYCC4wIXOv(T9 z0ComWB}v@BfLS);A!xv*_{_$3m=D6<P_kQcTLqzH zb59vKU^D~jqnYXHw^30>L#H!d-#XRo^B;1tVme=v8duuqS{&%9v`Gtle>#{bdxJ}1Rm*dXs(#cVK-D%Z)A@J3b%)5KVZ_)T0XU-;5^^;4X$7bNgSFNHcQ-Vbf;(&VM{IBBBm?;>^2UK=1f=U@bk{1;M&>Hcq!vj0@w;#jtkHB$I* zYSgZAhcUgPY*1~PYED{?^N6!gc0m)fYp@PW9N4!IyYxxA6tAW01Syvc1z8OewR9Oxk28EeBfm^C7N*cAN(jUTH0a^RAvnCsSvpUpTfey_lZSO$3B zKorucB2%sJ2%SX2uLG!9PltfG`X6$#16fk<+(88fG65XK70&zXvl$s8QP=62Z~wf~ z3Cc0JMP))@vlngL^?};A=|@OyD0s z3AhL-ZQxME;ZF+)grBq0f&c`ngM$YT#<&n1H6jrkUk$LOAR*?wjn*0k= zQvLO+ED8)%dXGXzh>Sni5w@#a?f&6VgO#m(k7+q<jz>)SjM=!W7~}kd-k(8l z)!@SyBxWnZPFUbMs+Gri>`bpk=-fB$+O#<5$x=eY#pUV$gMlqU|1_@a6LY4{nJ&Ng zO5D5S{t}qoRq=!E{tMgePR@pDNpIgBBP+kYFw9SbUBg;QWATNV<&u#enPB)>{YXxF zJ2yWM!T1YAcD$OH+fk*>(a}*bAAl@{OF!ZDbgZzsmHtMaR&j5}EU7KOtF6`^yqDGo zV4%?qWPd#G{Wp|}dox0Wngtw-;zhioGa&u53psDTpFFygsNf>};CQ@mu zt4m+tCNQ@xT0<)!h^`9a5bDx@Yyny%cY=)L_e*e!;S5JsMMmKO>7=O4kpYZ$x#apBh}ioQ~*vuo;Xq>%1pz@ zVr(ZTY?yV+|H0YCbqh>XWd%%}#IiQBE(8btmjK>V4*0U>kYq8xV6dauV-rn@G zKj*e1ELFSlsD8TjmQZdgW>YAEt5E#Hg>?XGqb}TWR#x`UTAQmoU7R$)+42#Gmd+XX z=X=|cZTVt!4zQRnZ)Lh~n)qQE<&ph;cmjo5rlSrI4};;5IkzyFPtsTbUP(!Yc1OqE zpmFw`m{{#B6k=>b13>Dj6up11)Jc5YE4t?qEz{Ej$AlFTc1al#Z?%AWQ9~UNh)#Yp z&2E*VjEtsf+a~zv0V79TczHrzUqWNAe?R~T6E5AmS7wxCrRuplmPEA@Pu9=9z|XH_ z=Y93+lex~yccVGpM8fF?D_dJ_3uaJ0amSU(F{ulhIR%XiIX#@G;NGN^YPe+?S&siw zyMAMgNf=G3t0U$t6lA_R;|-@0z=cIZ&(l$K;)nu9!nadpvE*G6J~uVQXwmybVH>)- zD0RSAMcmlZww=Tm4bxxZOJI@JHhgY$9|4RoDJ$hQ_4P!#q0#0zN}A}H8>n;vGEp^M zUzw%M0$L4RHehX_v*zDHvm5avN)|BW!oO^b?@t8oY#|yq4u-XR=eX?Zg+!> z;AFgvrF+s-2xmVB1!UOZb#r)hBB);k3yXbaQpTL?e%^4-%FPL5-lrx{CG@Z zpg8IL98aV=NY$P&yu{C4frAnRoJ;g-Q8G>os()9>&m~W|_ zzV(hf=dd>vm4Hj#wzdu~xBeyS(-n2L33yijETle7aISP zmHvQWR;ciN?9VhzRzx}hZQVVBpOV5fI#Ax|VlXcfyKaw*X9(2W%SVQvZ7cd6VzMb6 zXn}95OQTIeE_Py1LS*=(>A*j)1Mrz=cVB^2Z8*Xq7SfMK)z(cQDaKX$z5fmnx|01| zXSzK-dQ=m8X7{dLf|xoi77G#FMFH*5omYP&DpT$OX!C-F1q3uxpZoj2Iw|T4(2B|? zcrZc&6HEf82{FT%x-FPQ*qIot!6Hpz6^JcQOH+RiS%;*r>*=`yK;F=e3AZYs?nPr1 zu3TXa{R!jdqzSyqogt>hf(~|eb{{AdS65d-m4eLAqnoW9i9RhIVAT;xE&6P4!AFGn z7=SfVmSni~Q7TRMRgAx@HG$>lPB02IHb5m^Fh$1Wd!iMzO%f1W$e{} zD~YO*Ic`U2=9QE{ZT$nRfJFE|TIZI5y@GS0wxA@{oOABoygOKPXrPQ=P_RGr zA{a~WW)?OCZzczHuuKsXNZ|qZBy~FFp9BZvNkGSCHquxn>>~vQP|IFt7$ra+2$sMP zVq#*z%*+!LAaxb}?CI0jU<~s5H6VUBKR(>~O&4{5mLE#NOX%(syu7?3A|l6+-+)rR zOSh&|tnvG+ICqaH-Q(k*rk7U5X(@=kHwtCGE^`5<6E11&gs1gA%g>`hP37}=84q7xzts!?F(b7kQe{{HpXkbo1%B8@Wl+JW-T!`uIj+{GrMUWU|eFf zDMr7irGoz5TGYume9Y#i^gzJ$X$Lz#L>3zHc6J{_>=P0U^av1gD7ad*JVfFr-0Qxmc1* zkMfP#3v1%_+krZ$jl>r^r$7H4cAVkQ8`ND<$%R7EVYhb{dHc3^8+o<8Fqw$VD>T0K zuIahkUM5zg^QMygZHD>4_($RX1i(=ba7eX2@*=tPK;NC^wP<~rAD&(iRn6^&Ye@`r z$z$=D>0Ml2J}oS)fR@&K)7#mAtup$N{eT6eYnuy6_tyuG^;jvhX#PkGcY-Ja^KI0& zHc>+wlxKvf{XiubB(ej0_l|Q>Itacn4;KzP|D*#;2fjyU?jEx}p!FOiED+@9gLYk9 z=yQFEg~AWSw;YFHsWE4XP6oyz+9oDoV?6^xtXv~>zN$Kg;tBFVm_|WPA*Dkmw-|#i z{zw9R6fAj0ra0l5A082%AmQMJ6oc6Bz@QbP$=z6W8}iiC3!kKXM2y^~fqUSNc=6&o z^nW8?!ZBDZRllaouM2cZ^v5oKYj5`e)&x?ExaAb{ZI4(4&z>{}m}cs>LqhKn&;H3S-$EzfLHf{N*~3(BMG{d`5<`{KEdPE^*06KiDR? zm);z}raC||(*xx9uj;YpxN1Pz0lvDz0nt_)1c{#mqB@)yKBvhY5cIz;yWtAuw^nFt z!QD0YXLmIX4cEcU1v%&%RkA;!%%||ja^LcO)odi0HhQ5sg)$8cb7QouL$1EZWXFl> ztB$NfX6V=4GFC0|6MrjJz^TK;JlsF%d<&W;7*Ao?w=ZM?e8mVDu5UKh);V!jj*dTb znYlB83?5o>X_Vn>DbLH(Q3;dwNXyZdECc!-Xq)GG%nbQi*hva*!aPLys># ze*Bn_E8^6Dg*pdZ?w-TEh-y}A4rnGsI$?Rmw}*V?PC9&=yycEiHAKwaM(PZm*!EGV77|B4G9?IR}zcl~tgPf;mBt;YbC9{5)>pfaxz>^>LJJbIH{YN z#ffI02U#3?ksj$v0$j9B{0~R!R<+!kdO`}>&YWJfs_Blk8pg(H@d?$7t$3UK2ZLZ_ zi@p9QfBvfl@q&6(suNqz5^NLYl+rELka%mrt;#*s3w++p%*=o=w6RlvRCWpCzh{!D z`>&g?z~~dL9mFX^niLE%2Dc$eP(pB5y*QT6E(>qEqhm&Sc{vQ$ifSlV;Lw1IVp_&S zNjhY#cUvtj6UgiBp4Ng;SSSgm+*0^07WI>rQZ-$(gD-*yY)%3Uyi$f>+y(O__l=2x z-~M^vd0IKo@qbJVRMCHGkowVj0)I8B7l~;wp-i}9VKEFQ{DEhslkvHB?fUjhm#MYx zQOP3`G7l1T3ykl{Y&Tyz?IWXNpTc#DeSY^{+lw~4PAZOSabj?h%2A)L6-n03zlI#`1f{Sgq{aF z2TWVZn>SwPs0x_;2WnRCNBh4-X0`<;+M}|GZ?IJjNezC*^)|BVQZ> zxEw4E!n#DCPsk5gn)&*%T@38+)40ppvkESsBi|l^WaH0?WpaVZ713NnELIH2EK9;H zkcs=-&_|Dq&di)13l3(alET7RCAoIs%*_)98ygzlo_C6M7Uba>%569P8kFd)qIW}Z zk6UIO{uS5i#Q1o82ZU{s+mmO{koi`IJY!2k9Ua~s*5C}_vdirU8eov5jM;?mWrCZAv2uMt=UaG;Qg*&w2#wEB#B4I#-a62L;jh{pIo(~Qe zhIHV(5@EP$pC1;)%;fG5iRX&xo6lZr4BTa8)$WNQ7RjMKRH`MvjAiWRQSrOP5*;AQ z2@3nlvX=z}7!7yaiTe3LU|_uJX-;rme%XB`lOO+EV*k%dY|iuMKL4yMD~4Nvm4}|; zZ}}{c19Nfp1Gpk4J?%~h+BOaVB9rww03W{9?x-B1eWUQ$(qdQth7 zQ~JV@;;atos9rSX{Qs{?zij!@pc{p{VDOJBR&70|7MMvWaiMVR#qT^Mg9*7C?7{yq zED-*r{(Tf`La2)}>6}#E7H4D|XF^_e@tstopT66IIo(^PW{E7ZA|Wls!}BPV z9DMnZ`L%<)|NQr!{y$Z1=HC}!_W5@=vIDuycKfj1Kp;PDHx($8SE+v;4fO=#m0 zCU%*DiabmCqWSQ0DFZ`IuhPtSuheN#9?olh66uC3q@NuuX*;Dn>=Pf7r1SS}ojQ1| zw-j4HMThE5#`9E78AoSrvZ(4_-)f37XqB#8w(H_y3{Ru=W%#UbR$sSjTles`grek3 z??q!)k2wua7k1Ql8A?GON#UGR(zEu-DesBYMORjoD;>+GUdE3e;$NK%UH!vpxkrqL z#dn}y=vGMxl}$;V<)1ANC~_@(pOTW2l{E)^s+*hJ!ZB1%R7pzX!E}z&)Nu6D*tG27 z4>U+kTbg^Xd&aCt+qP%DxF&>89cVKBQ_OqPKk1-upEOyYa=AtB{xloyag5t{hc7UJ zvNJ0fUBlcwcZlQ-(9-ji07`sj8HEydfnDg?TKV*-BsIEpvgv_5og?b!1#_S5{_fmt zW{-~_ik4D8k0p-*7~NQ3k2MqG*n<*wbW~-wrq&x&%mw~_c;s5GNwMvXzymMTR8_kH z#9=cCcSE7FTMZ_{MclXYrlS)6Jdl;S&wPi?mxolCnt;;X+Sa_54HY&t7tZBson+fR zkjDrstRy(WRu|i92d+{ngM+sYRfA?7?m+h5dckMYA3~BSk#PFaGuoGXYYBN3*T%*}^}hheLW z4nq}dbYI`A?m3S3JeM3^>$h*YvL&r8C3n0u+qFv*+5DjCozbicRgVhI zuFeYjv@Yn=W1t$$%y(ocx;?_}C9BcR!ncRY%r;JkDjSxqG}6{Oat}0$e_RMSnUuG2 zLV?3GGQ1RutxMGcW~G<-#xsT+j5P1vf8Na`(C~$W=M-t_03AXi@oMc-0UP8hrH^=55jtTbb zC*?(qIG0)+{j;x(D-G+5@r*n(x$7mQkAi;dllN<0&2ODAHkVr{%70yB5u(_cs~7MT zqyx}Xdp%HZjh{@jS&V2=uPUOZ>TpJ2t5WG-x(0l)v)(;#nA&c;1E33tGPoW_7Q3>h zEZkI^gv3IBOm2~mbihjy*%sULfPDC_5i^Zr*5F$S+_l=C;6~nv=1F)Is|RWKk*rPT zuTgFC9zeMKdD;Bi+{)6%Ox+tcwsMnj-TU{oex&lqJ{~AuuAF`9p(|J%oyhLY%bqWc zoWH18yd7;+?`2~a($OIrfA0CnMs%88QhEc4)mgYt~ zg*IP@pOjQBD~OL=Evn%v)@!!1vDmb9`}FI+&k03!PD+56<>v;fzvb4wF0}c#B6i4J z1sL?iPN~JA18d?N?RCct{oK+723CH(%EgMsy2@JJQGC2k%n6JCa=6&`ZO{7&pHf(f zrz6p6mGa`bI&+&&`#JG*y(ue8(Q7Vo<gQq~iye9b9~GvA_T{`1k&f-1)7h`li-RL!c$Z8%zMSL!>#@yn zXJAp{II`XyBz$oV^5z9$s%#NAlNh{=0y=%|GvhYQB;(r6oEZ+WgfkHz*Q?iS9%tQ} zt^*KpOkm^%!ZW2Qf}P@m4g?X;FwxD?EmH`4Jq0)DAxn2DHP=?89csqwYtqo(jamMobmle6fYL(b==7PD)07*o(5` z6Oy7&9v}YuqU{a{Mi;K5_pJ?hczk^SCc}0hZRSJ`8vD|^AGw`) zaGfJd*-v)~GhyTUOo-uc{b7+y{l~nwp5Acl%l6PT*z+CrJ(;p%T=(WVeYrmM>SgP} zNTxAmD{F1g$y%D4ngTTp-&167pZ3}Goixpdo$l2ci4`2(ZP9>cA zMnCQz8TW8h(T4mH@nD0tBN?o(Rm$mV%u&cspVcUjtCJKpP55s5@- zN{jOn6}QAoFmLm_MNV@$4J;Y45#EzFc-2oGksgD{Ps2kopOG%Oqq)$P-&8%d&ii~N zgFbac40(CDb4awz0aq`6wY0QMx)+&cZe-}=IXRqgl<~F0w=Ax_fxH)7`P_D^N=L8T z#ahF96cnD~c=G(8nDRlY5RM6S9+UvL=iP#(@mx>nVA#?4F|qAG!mM&a88wXF(% z?bH9gBhx*aOsFm+b`Mj+yeYC>b`%_191s8uG(pjeJUP(77@B^woW49B%$8vM-@VFd z?W(R<@)xI7T|*i-KL2^(WkFPd+_7R=%AGSvO`!j~9NdO~5or3KRb5HM)^4WQpm%I} SmpYL0PWh_(m9)z@9{wNTjRR-^ diff --git a/shopfloor/services/single_pack_transfer.py b/shopfloor/services/single_pack_transfer.py index 64e2bfd3ec..38f30e8fb4 100644 --- a/shopfloor/services/single_pack_transfer.py +++ b/shopfloor/services/single_pack_transfer.py @@ -41,9 +41,9 @@ def _data_after_package_scanned(self, package_level): def _response_for_start(self, message=None, popup=None): return self._response(next_state="start", message=message, popup=popup) - def _response_for_confirm_start(self, package_level, message=None): + def _response_for_confirm_start(self, package_level, message=None, barcode=""): data = self._data_after_package_scanned(package_level) - data["confirmation_required"] = True + data["confirmation_required"] = barcode return self._response( next_state="start", data=data, @@ -51,7 +51,7 @@ def _response_for_confirm_start(self, package_level, message=None): ) def _response_for_scan_location( - self, package_level, message=None, confirmation_required=False + self, package_level, message=None, confirmation_required=None ): data = self._data_after_package_scanned(package_level) data["confirmation_required"] = confirmation_required @@ -61,7 +61,7 @@ def _response_for_scan_location( message=message, ) - def _scan_source(self, barcode, confirmation=False): + def _scan_source(self, barcode, confirmation=None): """Search a package""" search = self._actions_for("search") location = search.location_from_scan(barcode) @@ -93,7 +93,7 @@ def _scan_source(self, barcode, confirmation=False): return (None, package) - def start(self, barcode, confirmation=False): + def start(self, barcode, confirmation=None): picking_types = self.picking_types message, package = self._scan_source(barcode, confirmation) if message: @@ -164,9 +164,11 @@ def start(self, barcode, confirmation=False): message=self.msg_store.no_putaway_destination_available() ) - if package_level.is_done and not confirmation: + if package_level.is_done and confirmation != barcode: return self._response_for_confirm_start( - package_level, message=self.msg_store.already_running_ask_confirmation() + package_level, + message=self.msg_store.already_running_ask_confirmation(), + barcode=barcode, ) if not package_level.is_done: package_level.is_done = True @@ -205,7 +207,7 @@ def _create_package_level(self, package): def _is_move_state_valid(self, moves): return all(move.state != "cancel" for move in moves) - def validate(self, package_level_id, location_barcode, confirmation=False): + def validate(self, package_level_id, location_barcode, confirmation=None): """Validate the transfer""" search = self._actions_for("search") @@ -235,12 +237,12 @@ def validate(self, package_level_id, location_barcode, confirmation=False): package_level, message=self.msg_store.dest_location_not_allowed() ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != location_barcode and self.is_dest_location_to_confirm( package_level.location_dest_id, scanned_location ): return self._response_for_scan_location( package_level, - confirmation_required=True, + confirmation_required=location_barcode, message=self.msg_store.confirm_location_changed( package_level.location_dest_id, scanned_location ), @@ -298,7 +300,7 @@ class SinglePackTransferValidator(Component): def start(self): return { "barcode": {"type": "string", "nullable": False, "required": True}, - "confirmation": {"type": "boolean", "required": False}, + "confirmation": {"type": "string", "required": False}, } def cancel(self): @@ -310,7 +312,7 @@ def validate(self): return { "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, "location_barcode": {"type": "string", "nullable": False, "required": True}, - "confirmation": {"type": "boolean", "required": False}, + "confirmation": {"type": "string", "required": False}, } @@ -369,7 +371,7 @@ def _schema_for_package_level_details(self, required=False): def _schema_confirmation_required(self): return { "confirmation_required": { - "type": "boolean", + "type": "string", "nullable": True, "required": False, }, diff --git a/shopfloor/tests/test_single_pack_transfer.py b/shopfloor/tests/test_single_pack_transfer.py index 08bb5bca41..b179acf0b0 100644 --- a/shopfloor/tests/test_single_pack_transfer.py +++ b/shopfloor/tests/test_single_pack_transfer.py @@ -122,7 +122,7 @@ def test_start(self): next_state="scan_location", data=dict( self._response_package_level_data(package_level), - confirmation_required=False, + confirmation_required=None, ), ) @@ -199,7 +199,7 @@ def test_start_no_operation_create(self): ), "picking": self.data.picking(package_level.picking_id), "products": self.data.products(self.product_a), - "confirmation_required": False, + "confirmation_required": None, } self.assert_response(response, next_state="scan_location", data=expected_data) @@ -408,7 +408,7 @@ def test_start_already_started(self): }, data=dict( self._response_package_level_data(package_level), - confirmation_required=True, + confirmation_required=barcode, ), ) @@ -730,7 +730,7 @@ def test_validate_location_to_confirm(self): message=message, data=dict( self._response_package_level_data(package_level), - confirmation_required=True, + confirmation_required=sub_shelf2.barcode, ), ) @@ -765,7 +765,7 @@ def test_validate_location_with_confirm(self): "package_level_id": package_level.id, "location_barcode": self.shelf2.barcode, # acknowledge the change of destination - "confirmation": True, + "confirmation": self.shelf2.barcode, }, ) @@ -998,7 +998,7 @@ def test_start_package_unreserve_ok(self): next_state="scan_location", data=dict( self.service._data_after_package_scanned(new_package_level), - confirmation_required=False, + confirmation_required=None, ), ) self.assertRecordValues( From 24f62a4177ef0bab2b8969d836cb6b49feca24a7 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 8 Aug 2023 11:51:54 +0200 Subject: [PATCH 03/11] sf_mobile: imp single_pack_transfer confirm --- .../static/wms/src/scenario/single_pack_transfer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/single_pack_transfer.js b/shopfloor_mobile/static/wms/src/scenario/single_pack_transfer.js index 86117fea66..bc3f8c1fb3 100644 --- a/shopfloor_mobile/static/wms/src/scenario/single_pack_transfer.js +++ b/shopfloor_mobile/static/wms/src/scenario/single_pack_transfer.js @@ -24,7 +24,7 @@ export var SinglePackStatesMixin = { this.wait_call( this.odoo.call("start", { barcode: scanned.text, - confirmation: data.confirmation_required, + confirmation: data.confirmation_required || "", }) ); }, @@ -43,7 +43,7 @@ export var SinglePackStatesMixin = { package_level_id: data.id, location_barcode: scanned.text, confirmation: - confirmation || data.confirmation_required, + confirmation || data.confirmation_required || "", }) ); }, From 679698bb566e0e0bb7cfc2096839eeb5215b2ffc Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 8 Aug 2023 15:08:10 +0200 Subject: [PATCH 04/11] shopfloor: imp location_content_tranfer confirm --- .../services/location_content_transfer.py | 32 +++++++++---------- shopfloor/tests/common.py | 1 + .../test_location_content_transfer_base.py | 8 ++--- ...on_content_transfer_set_destination_all.py | 26 +++++++++++++-- ...ransfer_set_destination_package_or_line.py | 10 +++--- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/shopfloor/services/location_content_transfer.py b/shopfloor/services/location_content_transfer.py index 39a12ed6dd..9b8112acf9 100644 --- a/shopfloor/services/location_content_transfer.py +++ b/shopfloor/services/location_content_transfer.py @@ -80,7 +80,7 @@ def _response_for_scan_location(self, location=None, message=None): ) def _response_for_scan_destination_all( - self, pickings, message=None, confirmation_required=False + self, pickings, message=None, confirmation_required=None ): """Transition to the 'scan_destination_all' state @@ -116,7 +116,7 @@ def _response_for_start_single(self, pickings, message=None, popup=None): ) def _response_for_scan_destination( - self, location, next_content, message=None, confirmation_required=False + self, location, next_content, message=None, confirmation_required=None ): """Transition to the 'scan_destination' state @@ -461,7 +461,7 @@ def _lock_lines(self, lines): """Lock move lines""" self._actions_for("lock").for_update(lines) - def set_destination_all(self, location_id, barcode, confirmation=False): + def set_destination_all(self, location_id, barcode, confirmation=None): """Scan destination location for all the moves of the location barcode is a stock.location for the destination @@ -489,11 +489,11 @@ def set_destination_all(self, location_id, barcode, confirmation=False): return self._response_for_scan_destination_all( pickings, message=self.msg_store.dest_location_not_allowed() ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( move_lines.location_dest_id, scanned_location ): return self._response_for_scan_destination_all( - pickings, confirmation_required=True + pickings, confirmation_required=barcode ) self._lock_lines(move_lines) @@ -663,7 +663,7 @@ def scan_line(self, location_id, move_line_id, barcode): ) def set_destination_package( - self, location_id, package_level_id, barcode, confirmation=False + self, location_id, package_level_id, barcode, confirmation=None ): """Scan destination location for package level @@ -697,11 +697,11 @@ def set_destination_package( package_level, message=self.msg_store.dest_location_not_allowed(), ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( package_level.location_dest_id, scanned_location ): return self._response_for_scan_destination( - location, package_level, confirmation_required=True + location, package_level, confirmation_required=barcode ) package_move_lines = package_level.move_line_ids self._lock_lines(package_move_lines) @@ -722,7 +722,7 @@ def set_destination_package( ) def set_destination_line( - self, location_id, move_line_id, quantity, barcode, confirmation=False + self, location_id, move_line_id, quantity, barcode, confirmation=None ): """Scan destination location for move line @@ -754,11 +754,11 @@ def set_destination_line( return self._response_for_scan_destination( location, move_line, message=self.msg_store.dest_location_not_allowed() ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( move_line.location_dest_id, scanned_location ): return self._response_for_scan_destination( - location, move_line, confirmation_required=True + location, move_line, confirmation_required=barcode ) self._lock_lines(move_line) @@ -983,7 +983,7 @@ def set_destination_all(self): return { "location_id": {"coerce": to_int, "required": True, "type": "integer"}, "barcode": {"required": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } def go_to_single(self): @@ -1008,7 +1008,7 @@ def set_destination_package(self): "location_id": {"coerce": to_int, "required": True, "type": "integer"}, "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}, "barcode": {"required": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } def set_destination_line(self): @@ -1017,7 +1017,7 @@ def set_destination_line(self): "move_line_id": {"coerce": to_int, "required": True, "type": "integer"}, "quantity": {"coerce": to_float, "required": True, "type": "float"}, "barcode": {"required": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } def postpone_package(self): @@ -1084,7 +1084,7 @@ def _schema_all(self): "package_levels": self.schemas._schema_list_of(package_level_schema), "move_lines": self.schemas._schema_list_of(move_line_schema), "confirmation_required": { - "type": "boolean", + "type": "string", "nullable": True, "required": False, }, @@ -1099,7 +1099,7 @@ def _schema_single(self): "package_level": self.schemas._schema_dict_of(schema_package_level), "move_line": self.schemas._schema_dict_of(schema_move_line), "confirmation_required": { - "type": "boolean", + "type": "string", "nullable": True, "required": False, }, diff --git a/shopfloor/tests/common.py b/shopfloor/tests/common.py index 258dcebf06..af904a95ec 100644 --- a/shopfloor/tests/common.py +++ b/shopfloor/tests/common.py @@ -26,6 +26,7 @@ def setUpClassVars(cls): cls.input_location = cls.env.ref("stock.stock_location_company") cls.shelf1 = cls.env.ref("stock.stock_location_components") cls.shelf2 = cls.env.ref("stock.stock_location_14") + cls.shelf3 = cls.shelf2.sudo().copy({"barcode": "26019853"}) @classmethod def _shopfloor_user_values(cls): diff --git a/shopfloor/tests/test_location_content_transfer_base.py b/shopfloor/tests/test_location_content_transfer_base.py index 6559e7b07b..68675d97ef 100644 --- a/shopfloor/tests/test_location_content_transfer_base.py +++ b/shopfloor/tests/test_location_content_transfer_base.py @@ -58,7 +58,7 @@ def assert_response_start(self, response, message=None, popup=None): ) def _assert_response_scan_destination_all( - self, state, response, pickings, message=None, confirmation_required=False + self, state, response, pickings, message=None, confirmation_required=None ): # this code is repeated from the implementation, not great, but we # mostly want to ensure the selection of pickings is right, and the @@ -80,7 +80,7 @@ def _assert_response_scan_destination_all( ) def assert_response_scan_destination_all( - self, response, pickings, message=None, confirmation_required=False + self, response, pickings, message=None, confirmation_required=None ): self._assert_response_scan_destination_all( "scan_destination_all", @@ -105,7 +105,7 @@ def assert_response_start_single( ) def _assert_response_scan_destination( - self, state, response, next_content, message=None, confirmation_required=False + self, state, response, next_content, message=None, confirmation_required=None ): location = next_content.location_id data = self.service._data_content_line_for_location(location, next_content) @@ -118,7 +118,7 @@ def _assert_response_scan_destination( ) def assert_response_scan_destination( - self, response, next_content, message=None, confirmation_required=False + self, response, next_content, message=None, confirmation_required=None ): self._assert_response_scan_destination( "scan_destination", diff --git a/shopfloor/tests/test_location_content_transfer_set_destination_all.py b/shopfloor/tests/test_location_content_transfer_set_destination_all.py index 952ba9e985..adb1c8aad6 100644 --- a/shopfloor/tests/test_location_content_transfer_set_destination_all.py +++ b/shopfloor/tests/test_location_content_transfer_set_destination_all.py @@ -247,7 +247,7 @@ def test_set_destination_all_dest_location_need_confirm(self): response, self.pickings, message=self.service.msg_store.need_confirmation(), - confirmation_required=True, + confirmation_required=self.shelf2.barcode, ) def test_set_destination_all_dest_location_confirmation(self): @@ -263,7 +263,7 @@ def test_set_destination_all_dest_location_confirmation(self): # picking type's default dest location, ask confirmation (second scan) # from the user "barcode": self.shelf2.barcode, - "confirmation": True, + "confirmation": self.shelf2.barcode, }, ) self.assert_response_start( @@ -274,6 +274,28 @@ def test_set_destination_all_dest_location_confirmation(self): ) self.assert_all_done(self.shelf2) + def test_set_destination_all_dest_location_confirm_different(self): + """Scanned different location on location confirmation. + + New confirmation is requested + """ + response = self.service.dispatch( + "set_destination_all", + params={ + "location_id": self.content_loc.id, + # expected shelf2 confirmation, but scanned shelf3 + # another confirmation is required + "barcode": self.shelf3.barcode, + "confirmation": self.shelf2.barcode, + }, + ) + self.assert_response_scan_destination_all( + response, + self.pickings, + message=self.service.msg_store.need_confirmation(), + confirmation_required=self.shelf3.barcode, + ) + def test_set_destination_all_dest_location_invalid(self): """The scanned destination location is not in the menu's picking types""" response = self.service.dispatch( 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 d780bfceeb..b16c958d68 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 @@ -140,20 +140,21 @@ def test_set_destination_package_dest_location_move_nok(self): def test_set_destination_package_dest_location_to_confirm(self): """Scanned destination location valid, but need a confirmation.""" + barcode = self.env.ref("stock.stock_location_14").barcode 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.env.ref("stock.stock_location_14").barcode, + "barcode": barcode, }, ) self.assert_response_scan_destination( response, package_level, message=self.service.msg_store.need_confirmation(), - confirmation_required=True, + confirmation_required=barcode, ) def test_set_destination_package_dest_location_ok(self): @@ -324,6 +325,7 @@ def test_set_destination_line_dest_location_move_nok(self): def test_set_destination_line_dest_location_to_confirm(self): """Scanned destination location valid, but need a confirmation.""" + barcode = self.env.ref("stock.stock_location_14").barcode move_line = self.picking2.move_line_ids[0] response = self.service.dispatch( "set_destination_line", @@ -331,14 +333,14 @@ def test_set_destination_line_dest_location_to_confirm(self): "location_id": self.content_loc.id, "move_line_id": move_line.id, "quantity": move_line.product_uom_qty, - "barcode": self.env.ref("stock.stock_location_14").barcode, + "barcode": barcode, }, ) self.assert_response_scan_destination( response, move_line, message=self.service.msg_store.need_confirmation(), - confirmation_required=True, + confirmation_required=barcode, ) def test_set_destination_line_dest_location_ok(self): From c43a6f8057e8c3468d65c2c532185a4c462936ca Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 8 Aug 2023 15:08:33 +0200 Subject: [PATCH 05/11] sf_mobile: imp location_content_transfer confirm --- .../static/wms/src/scenario/location_content_transfer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js b/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js index 371d6152c0..28ad5823ba 100644 --- a/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js +++ b/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js @@ -271,7 +271,7 @@ const LocationContentTransfer = { this.odoo.call("set_destination_all", { location_id: data.location.id, barcode: scanned.text, - confirmation: data.confirmation_required, + confirmation: data.confirmation_required || "", }) ); }, @@ -334,7 +334,7 @@ const LocationContentTransfer = { package_level_id: data.package_level.id, location_id: data.package_level.location_src.id, barcode: scanned.text, - confirmation: data.confirmation_required, + confirmation: data.confirmation_required || "", }; } else { endpoint = "set_destination_line"; @@ -342,7 +342,7 @@ const LocationContentTransfer = { move_line_id: data.move_line.id, location_id: data.move_line.location_src.id, barcode: scanned.text, - confirmation: data.confirmation_required, + confirmation: data.confirmation_required || "", quantity: this.scan_destination_qty, }; } From 4073eeadc070e4394ef5db0aea548b697f7f9384 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 9 Aug 2023 11:53:28 +0200 Subject: [PATCH 06/11] sf: imp cluster picking confirm unload all --- shopfloor/services/cluster_picking.py | 17 ++++++---- .../tests/test_cluster_picking_unload.py | 32 +++++++++++++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/shopfloor/services/cluster_picking.py b/shopfloor/services/cluster_picking.py index 721f835c72..71f46f1987 100644 --- a/shopfloor/services/cluster_picking.py +++ b/shopfloor/services/cluster_picking.py @@ -144,10 +144,10 @@ def _response_for_unload_all(self, batch, message=None): message=message, ) - def _response_for_confirm_unload_all(self, batch, message=None): + def _response_for_confirm_unload_all(self, batch, message=None, confirmation=None): return self._response( next_state="confirm_unload_all", - data=self._data_for_unload_all(batch), + data=self._data_for_unload_all(batch, confirmation=confirmation), message=message, ) @@ -814,13 +814,15 @@ def prepare_unload(self, picking_batch_id): # the lines have different destinations return self._unload_next_package(batch) - def _data_for_unload_all(self, batch): + def _data_for_unload_all(self, batch, confirmation=None): lines = self._lines_to_unload(batch) # all the lines destinations are the same here, it looks # only for the first one first_line = fields.first(lines) data = self.data.picking_batch(batch) data.update({"location_dest": self.data.location(first_line.location_dest_id)}) + if confirmation: + data.update({"confirmation": confirmation}) return data def _data_for_unload_single(self, batch, package): @@ -1068,7 +1070,7 @@ def change_pack_lot(self, picking_batch_id, move_line_id, barcode, quantity=None message=self.msg_store.no_package_or_lot_for_barcode(barcode), ) - def set_destination_all(self, picking_batch_id, barcode, confirmation=False): + def set_destination_all(self, picking_batch_id, barcode, confirmation=None): """Set the destination for all the lines of the batch with a dest. package This method must be used only if all the move lines which have a destination @@ -1109,10 +1111,10 @@ def set_destination_all(self, picking_batch_id, barcode, confirmation=False): batch, message=self.msg_store.dest_location_not_allowed() ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( first_line.location_dest_id, scanned_location ): - return self._response_for_confirm_unload_all(batch) + return self._response_for_confirm_unload_all(batch, confirmation=barcode) self._unload_write_destination_on_lines(lines, scanned_location) completion_info = self._actions_for("completion.info") @@ -1379,7 +1381,7 @@ def set_destination_all(self): return { "picking_batch_id": {"coerce": to_int, "required": True, "type": "integer"}, "barcode": {"required": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } def unload_split(self): @@ -1594,6 +1596,7 @@ def _schema_for_single_line_details(self): def _schema_for_unload_all(self): schema = self.schemas.picking_batch() schema["location_dest"] = self.schemas._schema_dict_of(self.schemas.location()) + schema["confirmation"] = {"type": "string", "nullable": True, "required": False} return schema @property diff --git a/shopfloor/tests/test_cluster_picking_unload.py b/shopfloor/tests/test_cluster_picking_unload.py index 9f0410de95..1dbef5843f 100644 --- a/shopfloor/tests/test_cluster_picking_unload.py +++ b/shopfloor/tests/test_cluster_picking_unload.py @@ -381,15 +381,17 @@ def test_set_destination_all_need_confirmation(self): self._set_dest_package_and_done(move_lines, self.bin1) move_lines.write({"location_dest_id": self.packing_a_location.id}) + barcode = self.packing_b_location.barcode response = self.service.dispatch( "set_destination_all", params={ "picking_batch_id": self.batch.id, - "barcode": self.packing_b_location.barcode, + "barcode": barcode, }, ) location = move_lines[0].location_dest_id data = self._data_for_batch(self.batch, location) + data["confirmation"] = barcode self.assert_response( response, next_state="confirm_unload_all", @@ -402,12 +404,13 @@ def test_set_destination_all_with_confirmation(self): self._set_dest_package_and_done(move_lines, self.bin1) move_lines.write({"location_dest_id": self.packing_a_location.id}) + barcode = self.packing_b_location.barcode response = self.service.dispatch( "set_destination_all", params={ "picking_batch_id": self.batch.id, - "barcode": self.packing_b_location.barcode, - "confirmation": True, + "barcode": barcode, + "confirmation": barcode, }, ) self.assertRecordValues( @@ -424,6 +427,29 @@ def test_set_destination_all_with_confirmation(self): message={"message_type": "success", "body": "Batch Transfer complete"}, ) + def test_set_destination_all_check_confirmation(self): + """Endpoint called confirming with a different location, ask confirmation again""" + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines, self.bin1) + move_lines.write({"location_dest_id": self.packing_a_location.id}) + + barcode = self.packing_b_location.barcode + response = self.service.dispatch( + "set_destination_all", + params={ + "picking_batch_id": self.batch.id, + "barcode": barcode, + "confirmation": "other_barcode", + }, + ) + data = self._data_for_batch(self.batch, self.packing_a_location) + data["confirmation"] = barcode + self.assert_response( + response, + next_state="confirm_unload_all", + data=data, + ) + class ClusterPickingUnloadSplitCase(ClusterPickingUnloadingCommonCase): """Tests covering the /unload_split endpoint From 1bfb5e6d51b8fc17b50e9eaeebfc9c2bd610486c Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 9 Aug 2023 11:55:51 +0200 Subject: [PATCH 07/11] sf_mobile: imp cluster picking confirm unload all --- .../static/wms/src/scenario/cluster_picking.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js index b85426a68a..fee78db6da 100644 --- a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js @@ -347,7 +347,7 @@ const ClusterPicking = { title: this.$t("cluster_picking.unload_all.title"), scan_placeholder: this.$t("scan_placeholder_translation"), }, - on_scan: (scanned, confirmation = false) => { + on_scan: (scanned, confirmation = "") => { this.state_set_data({location_barcode: scanned.text}); this.wait_call( this.odoo.call("set_destination_all", { @@ -363,23 +363,12 @@ const ClusterPicking = { title: this.$t("cluster_picking.confirm_unload_all.title"), scan_placeholder: this.$t("scan_placeholder_translation"), }, - on_user_confirm: (answer) => { - // TODO: check if this used - // -> no flag is set to enable the confirmation dialog, - // we only display a message, unlike `confirm_start` - if (answer == "yes") { - // Reuse data from unload_all - const scan_data = this.state_get_data("unload_all"); - this.state.on_scan(scan_data.location_barcode, true); - } else { - this.state_to("scan_destination"); - } - }, on_scan: (scanned, confirmation = true) => { this.on_state_exit(); // FIXME: use state_load or traverse the state // this.current_state_key = "unload_all"; // this.state.on_scan(scanned, confirmation); + confirmation = this.state.data.confirmation || ""; this.states.unload_all.on_scan(scanned, confirmation); }, }, From cde3f2e5f69e2c531950f368df90e7269c64b117 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 9 Aug 2023 13:25:15 +0200 Subject: [PATCH 08/11] sf: imp cluster picking confirm unload --- shopfloor/services/cluster_picking.py | 27 ++++++++++++------- .../tests/test_cluster_picking_unload.py | 13 +++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/shopfloor/services/cluster_picking.py b/shopfloor/services/cluster_picking.py index 71f46f1987..5101fd8fc0 100644 --- a/shopfloor/services/cluster_picking.py +++ b/shopfloor/services/cluster_picking.py @@ -166,10 +166,14 @@ def _response_for_unload_set_destination(self, batch, package, message=None): message=message, ) - def _response_for_confirm_unload_set_destination(self, batch, package): + def _response_for_confirm_unload_set_destination( + self, batch, package, confirmation=None + ): return self._response( next_state="confirm_unload_set_destination", - data=self._data_for_unload_single(batch, package), + data=self._data_for_unload_single( + batch, package, confirmation=confirmation + ), ) def find_batch(self): @@ -825,11 +829,13 @@ def _data_for_unload_all(self, batch, confirmation=None): data.update({"confirmation": confirmation}) return data - def _data_for_unload_single(self, batch, package): + def _data_for_unload_single(self, batch, package, confirmation=None): line = fields.first( package.planned_move_line_ids.filtered(self._filter_for_unload) ) data = self.data.picking_batch(batch) + if confirmation: + data.update({"confirmation": confirmation}) data.update( { "package": self.data.package(package), @@ -1217,7 +1223,7 @@ def unload_scan_pack(self, picking_batch_id, package_id, barcode): return self._response_for_unload_set_destination(batch, package) def unload_scan_destination( - self, picking_batch_id, package_id, barcode, confirmation=False + self, picking_batch_id, package_id, barcode, confirmation=None ): """Scan the final destination for all the move lines moved with the Bin @@ -1229,7 +1235,7 @@ def unload_scan_destination( * unload_single: line is processed and the next bin can be unloaded * confirm_unload_set_destination: the destination is valid but not the expected, ask a confirmation. This state has to call again the - endpoint with confirmation=True + endpoint with confirmation=barcode * start_line: if the batch still has lines to pick * start: if the batch is done. In this case, this method *has* to handle the closing of the batch to create backorders. @@ -1259,7 +1265,7 @@ def _lock_lines(self, lines): self._actions_for("lock").for_update(lines) def _unload_scan_destination_lines( - self, batch, package, lines, barcode, confirmation=False + self, batch, package, lines, barcode, confirmation=None ): # Lock move lines that will be updated self._lock_lines(lines) @@ -1273,10 +1279,12 @@ def _unload_scan_destination_lines( return self._response_for_unload_set_destination( batch, package, message=self.msg_store.dest_location_not_allowed() ) - if not confirmation and self.is_dest_location_to_confirm( + if confirmation != barcode and self.is_dest_location_to_confirm( first_line.location_dest_id, scanned_location ): - return self._response_for_confirm_unload_set_destination(batch, package) + return self._response_for_confirm_unload_set_destination( + batch, package, confirmation=barcode + ) self._unload_write_destination_on_lines(lines, scanned_location) @@ -1401,7 +1409,7 @@ def unload_scan_destination(self): "picking_batch_id": {"coerce": to_int, "required": True, "type": "integer"}, "package_id": {"coerce": to_int, "required": True, "type": "integer"}, "barcode": {"required": True, "type": "string"}, - "confirmation": {"type": "boolean", "nullable": True, "required": False}, + "confirmation": {"type": "string", "nullable": True, "required": False}, } @@ -1604,6 +1612,7 @@ def _schema_for_unload_single(self): schema = self.schemas.picking_batch() schema["package"] = self.schemas._schema_dict_of(self.schemas.package()) schema["location_dest"] = self.schemas._schema_dict_of(self.schemas.location()) + schema["confirmation"] = {"type": "string", "nullable": True, "required": False} return schema @property diff --git a/shopfloor/tests/test_cluster_picking_unload.py b/shopfloor/tests/test_cluster_picking_unload.py index 1dbef5843f..e1c2cc6aec 100644 --- a/shopfloor/tests/test_cluster_picking_unload.py +++ b/shopfloor/tests/test_cluster_picking_unload.py @@ -634,7 +634,7 @@ def test_scan_destination_unload_package_enabled(self): "picking_batch_id": self.batch.id, "package_id": self.bin1.id, "barcode": dest_location.barcode, - "confirmation": True, + "confirmation": dest_location.barcode, }, ) self.assertRecordValues( @@ -654,7 +654,7 @@ def test_scan_destination_unload_package_enabled(self): "picking_batch_id": self.batch.id, "package_id": self.bin2.id, "barcode": dest_location.barcode, - "confirmation": True, + "confirmation": dest_location.barcode, }, ) self.assertRecordValues( @@ -849,16 +849,18 @@ def test_unload_scan_destination_error_location_move_invalid(self): def test_unload_scan_destination_need_confirmation(self): """Endpoint called with a barcode for another (valid) location""" + barcode = self.packing_b_location.barcode response = self.service.dispatch( "unload_scan_destination", params={ "picking_batch_id": self.batch.id, "package_id": self.bin1.id, - "barcode": self.packing_b_location.barcode, + "barcode": barcode, }, ) location = self.bin1_lines[0].location_dest_id data = self._data_for_batch(self.batch, location, pack=self.bin1) + data["confirmation"] = barcode self.assert_response( response, next_state="confirm_unload_set_destination", @@ -867,13 +869,14 @@ def test_unload_scan_destination_need_confirmation(self): def test_unload_scan_destination_with_confirmation(self): """Endpoint called with a barcode for another (valid) location, confirm""" + barcode = self.packing_a_location.barcode response = self.service.dispatch( "unload_scan_destination", params={ "picking_batch_id": self.batch.id, "package_id": self.bin2.id, - "barcode": self.packing_a_location.barcode, - "confirmation": True, + "barcode": barcode, + "confirmation": barcode, }, ) self.assertRecordValues( From ac69f30a1c084c3c2cc3dda85ce688fec0896122 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 9 Aug 2023 13:25:37 +0200 Subject: [PATCH 09/11] sf_mobile: imp cluster picking confirm unload --- shopfloor_mobile/static/wms/src/scenario/cluster_picking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js index fee78db6da..2f6172f3e8 100644 --- a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js @@ -415,7 +415,7 @@ const ClusterPicking = { picking_batch_id: this.current_batch().id, package_id: null, // FIXME: where does it come from? backend data? barcode: scanned.text, - confirmation: true, + confirmation: this.state.data.confirmation || "", }) ); }, From a6d26e37d9e86f74dcda8315e14d5f95afc32adf Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Wed, 9 Aug 2023 14:41:02 +0200 Subject: [PATCH 10/11] sf_mobile: imp zone_picking confirm --- .../static/wms/src/scenario/zone_picking.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js index cf17005ae8..3c84b8ce5a 100644 --- a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js @@ -593,7 +593,7 @@ const ZonePicking = { scan_source(barcode) { let data = { barcode: barcode, - confirmation: this.state.data.confirmation_required, + confirmation: this.state.data.confirmation_required || "", }; if (this.state_is("select_line") && this.state.data.product) { data.product_id = this.state.data.product.id; @@ -841,7 +841,7 @@ const ZonePicking = { move_line_id: data.move_line.id, barcode: scanned.text, quantity: quantity, - confirmation: data.confirmation_required, + confirmation: data.confirmation_required || "", // package_id: data.is_complete_mix_pack ? data.move_line.package_src.id : null, handle_complete_mix_pack: data.handle_complete_mix_pack, }) @@ -871,7 +871,8 @@ const ZonePicking = { this.wait_call( this.odoo.call("set_destination_all", { barcode: scanned.text, - confirmation: this.state.data.confirmation_required, + confirmation: + this.state.data.confirmation_required || "", }) ); }, @@ -903,7 +904,8 @@ const ZonePicking = { this.odoo.call("unload_set_destination", { package_id: this.state.data.move_line.package_dest.id, barcode: scanned.text, - confirmation: this.state.data.confirmation_required, + confirmation: + this.state.data.confirmation_required || "", }) ); }, From fe180ada5eda2083d9e884bb39b068c7d085a9ec Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Thu, 10 Aug 2023 09:26:30 +0200 Subject: [PATCH 11/11] sf_checkout_sync: imp confirmation --- .../tests/test_shopfloor_zone_picking_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_checkout_sync/tests/test_shopfloor_zone_picking_sync.py b/shopfloor_checkout_sync/tests/test_shopfloor_zone_picking_sync.py index 93dd5242a6..de5e2f3377 100644 --- a/shopfloor_checkout_sync/tests/test_shopfloor_zone_picking_sync.py +++ b/shopfloor_checkout_sync/tests/test_shopfloor_zone_picking_sync.py @@ -64,7 +64,7 @@ def test_unload_set_destination_sync(self): params={ "package_id": self.free_package.id, "barcode": self.packing_sublocation.barcode, - "confirmation": True, + "confirmation": self.packing_sublocation.barcode, }, ) # check data