From af8a5b659b47f73c5103f6260449516b28247ab1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 20 Aug 2020 12:29:44 +0200 Subject: [PATCH 1/3] bknd: fix zone_picking handling of set destination * if qty is partial -> accept only packages as destination * fix message when nothing has been processed (no location and no pkg) --- shopfloor/services/zone_picking.py | 95 ++++++++++++++----- .../test_zone_picking_set_line_destination.py | 63 +++--------- 2 files changed, 84 insertions(+), 74 deletions(-) diff --git a/shopfloor/services/zone_picking.py b/shopfloor/services/zone_picking.py index 81292dd388..46f2ac86c4 100644 --- a/shopfloor/services/zone_picking.py +++ b/shopfloor/services/zone_picking.py @@ -1,7 +1,7 @@ import functools from odoo.fields import first -from odoo.tools.float_utils import float_compare +from odoo.tools.float_utils import float_compare, float_is_zero from odoo.addons.base_rest.components.service import to_bool, to_int from odoo.addons.component.core import Component @@ -461,13 +461,15 @@ def scan_source(self, zone_location_id, picking_type_id, barcode, order="priorit def _set_destination_location( self, zone_location, picking_type, move_line, quantity, confirmation, location ): + location_changed = False + response = None # 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 location.is_sublocation_of(move_line.location_dest_id) and ( not confirmation and location.is_sublocation_of(picking_type.default_location_dest_id) ): - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, @@ -476,6 +478,7 @@ def _set_destination_location( ), confirmation_required=True, ) + return (location_changed, response) # A valid location is a sub-location of the original destination, or a # sub-location of the picking type's default destination location if # `confirmation is True @@ -483,20 +486,22 @@ def _set_destination_location( confirmation and not location.is_sublocation_of(picking_type.default_location_dest_id) ): - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, message=self.msg_store.dest_location_not_allowed(), ) + return (location_changed, response) # If no destination package if not move_line.result_package_id: - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, message=self.msg_store.dest_package_required(), ) + return (location_changed, response) # destination location set to the scanned one move_line.location_dest_id = location # the quantity done is set to the passed quantity @@ -508,12 +513,14 @@ def _set_destination_location( # try to re-assign any split move (in case of partial qty) if "confirmed" in move_line.picking_id.move_lines.mapped("state"): move_line.picking_id.action_assign() + location_changed = True # Zero check zero_check = picking_type.shopfloor_zero_check if zero_check and move_line.location_id.planned_qty_in_location_is_empty(): - return self._response_for_zero_check( + response = self._response_for_zero_check( zone_location, picking_type, move_line.location_id ) + return (location_changed, response) def _is_package_empty(self, package): return not bool(package.quant_ids) @@ -528,41 +535,55 @@ def _is_package_already_used(self, package): ) ) + def _move_line_compare_qty(self, move_line, qty): + rounding = move_line.product_uom_id.rounding + return float_compare( + qty, move_line.product_uom_qty, precision_rounding=rounding + ) + + def _move_line_full_qty(self, move_line, qty): + rounding = move_line.product_uom_id.rounding + return float_is_zero( + move_line.product_uom_qty - qty, precision_rounding=rounding + ) + def _set_destination_package( self, zone_location, picking_type, move_line, quantity, package ): + package_changed = False + response = None # A valid package is: # * an empty package # * not used as destination for another move line if not self._is_package_empty(package): - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, message=self.msg_store.package_not_empty(package), ) + return (package_changed, response) if self._is_package_already_used(package): - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, message=self.msg_store.package_already_used(package), ) + return (package_changed, response) # the quantity done is set to the passed quantity # but if we move a partial qty, we need to split the move line - rounding = move_line.product_uom_id.rounding - compare = float_compare( - quantity, move_line.product_uom_qty, precision_rounding=rounding - ) + compare = self._move_line_compare_qty(move_line, quantity) qty_lesser = compare == -1 qty_greater = compare == 1 if qty_greater: - return self._response_for_set_line_destination( + response = self._response_for_set_line_destination( zone_location, picking_type, move_line, message=self.msg_store.unable_to_pick_more(move_line.product_uom_qty), ) + return (package_changed, response) elif qty_lesser: # split the move line which will be processed later remaining = move_line.product_uom_qty - quantity @@ -578,12 +599,14 @@ def _set_destination_package( move_line.result_package_id = package # the field ``shopfloor_user_id`` is updated with the current user move_line.shopfloor_user_id = self.env.user + package_changed = True # Zero check zero_check = picking_type.shopfloor_zero_check if zero_check and move_line.location_id.planned_qty_in_location_is_empty(): - return self._response_for_zero_check( + response = self._response_for_zero_check( zone_location, picking_type, move_line.location_id ) + return (package_changed, response) def set_destination( self, @@ -647,29 +670,50 @@ def set_destination( move_line = self.env["stock.move.line"].browse(move_line_id) if not move_line.exists(): return self._response_for_start(message=self.msg_store.record_not_found()) + + pkg_moved = False search = self.actions_for("search") - # When the barcode is a location - location = search.location_from_scan(barcode) - if location: - response = self._set_destination_location( - zone_location, picking_type, move_line, quantity, confirmation, location - ) - if response: - return response + accept_only_package = not self._move_line_full_qty(move_line, quantity) + + if not accept_only_package: + # When the barcode is a location + location = search.location_from_scan(barcode) + if location: + pkg_moved, response = self._set_destination_location( + zone_location, + picking_type, + move_line, + quantity, + confirmation, + location, + ) + if response: + return response + # When the barcode is a package package = search.package_from_scan(barcode) if package: location = move_line.location_dest_id - response = self._set_destination_package( + pkg_moved, response = self._set_destination_package( zone_location, picking_type, move_line, quantity, package ) if response: return response + + message = None + + if not pkg_moved and not package and accept_only_package: + message = self.msg_store.package_not_found_for_barcode(barcode) + return self._response_for_set_line_destination( + zone_location, picking_type, move_line, message=message + ) + + if pkg_moved: + message = self.msg_store.confirm_pack_moved() + # Process the next line response = self.list_move_lines(zone_location.id, picking_type.id) - return self._response( - base_response=response, message=self.msg_store.confirm_pack_moved(), - ) + return self._response(base_response=response, message=message,) def is_zero(self, zone_location_id, picking_type_id, move_line_id, zero): """Confirm or not if the source location of a move has zero qty @@ -1148,6 +1192,7 @@ def unload_set_destination( move_lines, message=self.msg_store.record_not_found(), ) + buffer_lines = self._find_buffer_move_lines( zone_location, picking_type, dest_package=package ) diff --git a/shopfloor/tests/test_zone_picking_set_line_destination.py b/shopfloor/tests/test_zone_picking_set_line_destination.py index bf2c13f50f..f1414d6823 100644 --- a/shopfloor/tests/test_zone_picking_set_line_destination.py +++ b/shopfloor/tests/test_zone_picking_set_line_destination.py @@ -188,13 +188,12 @@ def test_set_destination_location_no_other_move_line_partial_qty(self): Then the operator move 6 qty on 10, we get: - move qty 6 (done): - -> move_line qty 6 from location X - move qty 4 (assigned): - -> move_line qty 4 from location Y (remaining) + an error because we can move only full qty by location + and only a package barcode is allowed on scan. """ zone_location = self.zone_location picking_type = self.picking3.picking_type_id + barcode = self.packing_location.barcode moves_before = self.picking3.move_lines self.assertEqual(len(moves_before), 1) self.assertEqual(len(moves_before.move_line_ids), 1) @@ -207,30 +206,17 @@ def test_set_destination_location_no_other_move_line_partial_qty(self): "zone_location_id": zone_location.id, "picking_type_id": picking_type.id, "move_line_id": move_line.id, - "barcode": self.packing_location.barcode, + "barcode": barcode, "quantity": 6, "confirmation": False, }, ) - # Check picking data (move has been split in two, 6 done and 4 remaining) - moves_after = self.picking3.move_lines - self.assertEqual(len(moves_after), 2) - self.assertEqual(moves_after[0].product_uom_qty, 6) - self.assertEqual(moves_after[0].state, "done") - self.assertEqual(moves_after[0].move_line_ids.product_uom_qty, 0) - self.assertEqual(moves_after[1].product_uom_qty, 4) - self.assertEqual(moves_after[1].state, "assigned") - self.assertEqual(moves_after[1].move_line_ids.product_uom_qty, 4) - self.assertEqual(move_line.qty_done, 6) - # Check response - move_lines = self.service._find_location_move_lines(zone_location, picking_type) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) - self.assert_response_select_line( + self.assert_response_set_line_destination( response, zone_location, picking_type, - move_lines, - message=self.service.msg_store.confirm_pack_moved(), + move_line, + message=self.service.msg_store.package_not_found_for_barcode(barcode), ) def test_set_destination_location_several_move_line_full_qty(self): @@ -308,56 +294,35 @@ def test_set_destination_location_several_move_line_partial_qty(self): Then the operator move 4 qty on 6 (from the first move line), we get: - move qty 4 (done): - -> move_line qty 4 from location X - move qty 2 (assigned): - -> move_line qty 2 from location X (remaining) - move qty 4 (assigned): - -> move_line qty 4 from location Y (untouched) + an error because we can move only full qty by location + and only a package barcode is allowed on scan. """ zone_location = self.zone_location picking_type = self.picking4.picking_type_id + barcode = self.packing_location.barcode moves_before = self.picking4.move_lines self.assertEqual(len(moves_before), 1) # 10 qty self.assertEqual(len(moves_before.move_line_ids), 2) # 6+4 qty 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 - 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, + "barcode": barcode, "quantity": 4, # 4/6 qty "confirmation": False, }, ) - # Check picking data (move has been split in three, 4 done, 2+4 remaining) - moves_after = self.picking4.move_lines - self.assertEqual(len(moves_after), 3) - self.assertEqual(moves_after[0].product_uom_qty, 4) - self.assertEqual(moves_after[0].state, "done") - self.assertEqual(moves_after[0].move_line_ids.product_uom_qty, 0) - self.assertEqual(moves_after[1].product_uom_qty, 4) - self.assertEqual(moves_after[1].state, "assigned") - self.assertEqual(moves_after[1].move_line_ids.product_uom_qty, 4) - self.assertEqual(moves_after[2].product_uom_qty, 2) - self.assertEqual(moves_after[2].state, "assigned") - self.assertEqual(moves_after[2].move_line_ids.product_uom_qty, 2) - self.assertEqual(move_line.qty_done, 4) - self.assertNotEqual(move_line.move_id, other_move_line.move_id) - # Check response - move_lines = self.service._find_location_move_lines(zone_location, picking_type) - move_lines = move_lines.sorted(lambda l: l.move_id.priority, reverse=True) - self.assert_response_select_line( + self.assert_response_set_line_destination( response, zone_location, picking_type, - move_lines, - message=self.service.msg_store.confirm_pack_moved(), + move_line, + message=self.service.msg_store.package_not_found_for_barcode(barcode), ) def test_set_destination_location_zero_check(self): From 7b9dd20d4ae987319f9bad0ca4b8176cef306f5a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 20 Aug 2020 12:30:41 +0200 Subject: [PATCH 2/3] zone_picking: handle case where set line dest has no line --- shopfloor/services/zone_picking.py | 3 +++ shopfloor_mobile/static/wms/src/scenario/zone_picking.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/shopfloor/services/zone_picking.py b/shopfloor/services/zone_picking.py index 46f2ac86c4..9009725161 100644 --- a/shopfloor/services/zone_picking.py +++ b/shopfloor/services/zone_picking.py @@ -1239,6 +1239,9 @@ def unload_set_destination( return self._response_for_start( message=self.msg_store.picking_type_complete(picking_type) ) + # TODO: when we have no lines here + # we should not redirect to `unload_set_destination` + # because we'll have nothing to display (currently the UI is broken). return self._response_for_unload_set_destination( zone_location, picking_type, diff --git a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js index 02d8aa554b..a169648bb2 100644 --- a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js @@ -133,6 +133,7 @@ const template_mobile = ` :message="{body: 'Full order picking, no more operation.'}" /> +
+ + No line to process. +
Date: Thu, 20 Aug 2020 12:31:31 +0200 Subject: [PATCH 3/3] front: zone_picking + loc transfer fix qty handling on set dest Usecase: 1. set_line_destination is loaded -> the init dest qty is stored by `on_qty_update` 2. set a new qty via picker -> the dest qty is stored by `on_qty_update` 3. scan a wrong pack -> the state is reloaded, the qty stored is flushed, `on_qty_update` is not called because the picker widget is not re-rendered. By storing it in the component data, the value is preserved. --- .../static/wms/src/scenario/cluster_picking.js | 2 +- .../wms/src/scenario/location_content_transfer.js | 7 +++---- .../static/wms/src/scenario/zone_picking.js | 13 ++++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js index 0a35cd73be..b52c0d6bbd 100644 --- a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js @@ -254,7 +254,7 @@ export var ClusterPicking = Vue.component("cluster-picking", { ).quantity; }, on_qty_edit: qty => { - this.scan_destination_qty = parseInt(qty); + this.scan_destination_qty = parseInt(qty, 10); }, on_scan: scanned => { this.wait_call( 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 19bcbe3561..f2f99a1b0c 100644 --- a/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js +++ b/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js @@ -194,6 +194,7 @@ export var LocationContentTransfer = Vue.component("location-content-transfer", return { usage: "location_content_transfer", initial_state_key: "scan_location", + scan_destination_qty: 0, states: { init: { enter: () => { @@ -271,7 +272,7 @@ export var LocationContentTransfer = Vue.component("location-content-transfer", qty_edit: "on_qty_update", }, on_qty_update: qty => { - this.state.data.destination_qty = qty; + this.scan_destination_qty = parseInt(qty, 10); }, on_scan: scanned => { let endpoint, endpoint_data; @@ -291,9 +292,7 @@ export var LocationContentTransfer = Vue.component("location-content-transfer", location_id: data.move_line.location_src.id, barcode: scanned.text, confirmation: data.confirmation_required, - quantity: - this.state.data.destination_qty || - data.move_line.quantity, + quantity: this.scan_destination_qty, }; } this.wait_call(this.odoo.call(endpoint, endpoint_data)); diff --git a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js index a169648bb2..af63cf738e 100644 --- a/shopfloor_mobile/static/wms/src/scenario/zone_picking.js +++ b/shopfloor_mobile/static/wms/src/scenario/zone_picking.js @@ -376,6 +376,7 @@ export var ZonePicking = Vue.component("zone-picking", { usage: "zone_picking", initial_state_key: "scan_location", order_lines_by: "priority", + scan_destination_qty: 0, states: { scan_location: { display_info: { @@ -433,12 +434,19 @@ export var ZonePicking = Vue.component("zone-picking", { display_info: { title: "Set destination", scan_placeholder: "Scan location or package", + scan_placeholder_full: "Scan location or package", + scan_placeholder_partial: "Scan package", }, events: { qty_edit: "on_qty_update", }, on_qty_update: qty => { - this.state.data.destination_qty = qty; + this.scan_destination_qty = parseInt(qty, 10); + if (this.state.data.move_line.quantity != qty) { + this.state.display_info.scan_placeholder = this.state.display_info.scan_placeholder_partial; + } else { + this.state.display_info.scan_placeholder = this.state.display_info.scan_placeholder_full; + } }, on_scan: scanned => { const data = this.state.data; @@ -448,8 +456,7 @@ export var ZonePicking = Vue.component("zone-picking", { picking_type_id: this.current_picking_type().id, move_line_id: data.move_line.id, barcode: scanned.text, - quantity: - data.destination_qty || data.move_line.quantity, + quantity: this.scan_destination_qty, confirmation: data.confirmation_required, }) );