From 8c01934e1355dae1faca74a72440d95e51880f06 Mon Sep 17 00:00:00 2001 From: Joshua Lauer Date: Tue, 27 Jun 2023 14:38:17 +0100 Subject: [PATCH] [14.0][IMP] shopfloor: package checkout buttons Adds a menu config to allow/disallow the usage of packages --- shopfloor/actions/message.py | 24 ++ shopfloor/data/shopfloor_scenario_data.xml | 3 +- shopfloor/models/shopfloor_menu.py | 18 ++ shopfloor/services/checkout.py | 50 ++++- shopfloor/tests/test_checkout_base.py | 3 +- .../test_checkout_list_delivery_packaging.py | 5 +- shopfloor/tests/test_checkout_no_package.py | 4 +- .../test_checkout_scan_package_action.py | 207 +++++++++++++++++- shopfloor/tests/test_checkout_select_line.py | 18 +- .../test_checkout_select_package_base.py | 6 +- shopfloor/views/shopfloor_menu.xml | 7 + .../static/wms/src/scenario/checkout.js | 6 +- 12 files changed, 328 insertions(+), 23 deletions(-) diff --git a/shopfloor/actions/message.py b/shopfloor/actions/message.py index ea8844cb6c..3152b47652 100644 --- a/shopfloor/actions/message.py +++ b/shopfloor/actions/message.py @@ -807,3 +807,27 @@ def no_line_to_pack(self): "message_type": "warning", "body": _("No line to pack found."), } + + def invalid_scanned_checkout_object_wo_package( + self, scanned_object, package_process_type + ): + scanned_object_name = { + "package": _("a package"), + "packaging": _("a packaging"), + "delivery_packaging": _("a delivery packaging"), + }.get(scanned_object, _("N/A")) + + selection = self.env["shopfloor.menu"]._fields["package_process_type"].selection + ppt_name = None + for el in selection: + if el[0] == package_process_type: + ppt_name = el[1] + break + + return { + "message_type": "error", + "body": _( + "You scanned {scanned_object_name} which is not allowed" + " with the package process type '{ppt_name}'" + ).format(scanned_object_name=scanned_object_name, ppt_name=ppt_name), + } diff --git a/shopfloor/data/shopfloor_scenario_data.xml b/shopfloor/data/shopfloor_scenario_data.xml index c26c67c156..4c2faea3f3 100644 --- a/shopfloor/data/shopfloor_scenario_data.xml +++ b/shopfloor/data/shopfloor_scenario_data.xml @@ -43,7 +43,8 @@ { "no_prefill_qty": true, "show_oneline_package_content": true, - "auto_post_line": true + "auto_post_line": true, + "package_process_type": true } diff --git a/shopfloor/models/shopfloor_menu.py b/shopfloor/models/shopfloor_menu.py index 439c764b4e..b38e36e649 100644 --- a/shopfloor/models/shopfloor_menu.py +++ b/shopfloor/models/shopfloor_menu.py @@ -196,6 +196,17 @@ class ShopfloorMenu(models.Model): compute="_compute_auto_post_line_is_possible" ) + package_process_type_is_possible = fields.Boolean( + compute="_compute_package_process_type_is_possible" + ) + package_process_type = fields.Selection( + [ + ("without_package", "Without Package"), + ("with_package", "With Package"), + ], + "Package Proccess Type", + ) + @api.onchange("unload_package_at_destination") def _onchange_unload_package_at_destination(self): # Uncheck pick_pack_same_time when unload_package_at_destination is set to True @@ -411,3 +422,10 @@ def _compute_allow_alternative_destination_is_possible(self): menu.allow_alternative_destination_is_possible = ( menu.scenario_id.has_option("allow_alternative_destination") ) + + @api.depends("scenario_id") + def _compute_package_process_type_is_possible(self): + for menu in self: + menu.package_process_type_is_possible = menu.scenario_id.has_option( + "package_process_type" + ) diff --git a/shopfloor/services/checkout.py b/shopfloor/services/checkout.py index 10826b758a..c1630cfefd 100644 --- a/shopfloor/services/checkout.py +++ b/shopfloor/services/checkout.py @@ -83,19 +83,28 @@ def _response_for_manual_selection(self, message=None): return self._response(next_state="manual_selection", data=data, message=message) def _response_for_select_package(self, picking, lines, message=None): + with_pack, wo_pack = self._get_allow_package_options() return self._response( next_state="select_package", data={ "selected_move_lines": self._data_for_move_lines(lines.sorted()), "picking": self.data.picking(picking), "packing_info": self._data_for_packing_info(picking), - "no_package_enabled": not self.options.get( - "checkout__disable_no_package" - ), + "allow_with_package": with_pack, + "allow_without_package": wo_pack, }, message=message, ) + def _get_allow_package_options(self): + if not self.work.menu.package_process_type: + return True, True + + if self.work.menu.package_process_type == "with_package": + return True, False + + return False, True + def _data_for_packing_info(self, picking): """Return the packing information @@ -913,11 +922,37 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode): selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() search_result = self._scan_package_find(picking, barcode) + + message = self._check_scan_package_find_result(search_result) + if message: + return self._response_for_select_package( + picking, + selected_lines, + message=message, + ) + result_handler = getattr( self, "_scan_package_action_from_" + search_result.type ) return result_handler(picking, selected_lines, search_result.record) + def _check_scan_package_find_result(self, search_result): + ppt = self.work.menu.package_process_type + # Currently there is no known way to finish the checkout with a scan. To + # process without any package is only possible with a button. In its def + # a BadRequest is raised in case no package is not allowed + if not ppt or ppt == "with_package": + return + + stype = search_result.type + + if ppt == "without_package" and stype in [ + "package", + "packaging", + "delivery_packaging", + ]: + return self.msg_store.invalid_scanned_checkout_object_wo_package(stype, ppt) + def _scan_package_find(self, picking, barcode, search_types=None): search = self._actions_for("search") search_types = ( @@ -1087,7 +1122,7 @@ def no_package(self, picking_id, selected_line_ids): Transitions: * select_line: goes back to selection of lines to work on next lines """ - if self.options.get("checkout__disable_no_package"): + if self.work.menu.package_process_type == "with_package": raise BadRequest("`checkout.no_package` endpoint is not enabled") picking = self.env["stock.picking"].browse(picking_id) message = self._check_picking_status(picking) @@ -1562,7 +1597,12 @@ def _states(self): "select_package": dict( self._schema_selected_lines, packing_info={"type": "string", "nullable": True}, - no_package_enabled={ + allow_with_package={ + "type": "boolean", + "nullable": True, + "required": False, + }, + allow_without_package={ "type": "boolean", "nullable": True, "required": False, diff --git a/shopfloor/tests/test_checkout_base.py b/shopfloor/tests/test_checkout_base.py index deff3aea81..ca47423936 100644 --- a/shopfloor/tests/test_checkout_base.py +++ b/shopfloor/tests/test_checkout_base.py @@ -59,7 +59,8 @@ def _assert_select_package_qty_above(self, response, picking): ], "picking": self._picking_summary_data(picking), "packing_info": "", - "no_package_enabled": True, + "allow_with_package": True, + "allow_without_package": True, }, message={ "message_type": "warning", diff --git a/shopfloor/tests/test_checkout_list_delivery_packaging.py b/shopfloor/tests/test_checkout_list_delivery_packaging.py index 48fc87c446..01aab918a3 100644 --- a/shopfloor/tests/test_checkout_list_delivery_packaging.py +++ b/shopfloor/tests/test_checkout_list_delivery_packaging.py @@ -112,9 +112,8 @@ def test_list_delivery_packaging_not_available(self): self._move_line_data(ml) for ml in selected_lines.sorted() ], "packing_info": self.service._data_for_packing_info(self.picking), - "no_package_enabled": not self.service.options.get( - "checkout__disable_no_package" - ), + "allow_with_package": True, + "allow_without_package": True, }, message=self.service.msg_store.no_delivery_packaging_available(), ) diff --git a/shopfloor/tests/test_checkout_no_package.py b/shopfloor/tests/test_checkout_no_package.py index cb53452893..4a11616ee0 100644 --- a/shopfloor/tests/test_checkout_no_package.py +++ b/shopfloor/tests/test_checkout_no_package.py @@ -66,8 +66,8 @@ def test_no_package_ok(self): }, ) - def test_no_package_disabled(self): - self.service.work.options = {"checkout__disable_no_package": True} + def test_without_package_disabled(self): + self.menu.sudo().package_process_type = "with_package" with self.assertRaises(werkzeug.exceptions.BadRequest) as err: self.service.dispatch( "no_package", diff --git a/shopfloor/tests/test_checkout_scan_package_action.py b/shopfloor/tests/test_checkout_scan_package_action.py index 446f50debd..a5e851ad49 100644 --- a/shopfloor/tests/test_checkout_scan_package_action.py +++ b/shopfloor/tests/test_checkout_scan_package_action.py @@ -173,13 +173,83 @@ def test_scan_package_action_scan_package_keep_source_package_error(self): "picking": self.data.picking(picking), "selected_move_lines": self.data.move_lines(selected_lines), "packing_info": self.service._data_for_packing_info(picking), - "no_package_enabled": not self.service.options.get( - "checkout__disable_no_package" - ), + "allow_with_package": True, + "allow_without_package": True, }, message=self.service.msg_store.dest_package_not_valid(pack1), ) + def test_scan_package_action_scan_package_keep_source_package_error_package_process_type( + self, + ): + ppt = "without_package" + self.menu.sudo().package_process_type = ppt + picking = self._create_picking( + lines=[ + (self.product_a, 10), + (self.product_b, 10), + (self.product_c, 10), + (self.product_d, 10), + ] + ) + pack1_moves = picking.move_lines[:3] + pack2_moves = picking.move_lines[3:] + # put in 2 packs, for this test, we'll work on pack1 + self._fill_stock_for_moves(pack1_moves, in_package=True) + self._fill_stock_for_moves(pack2_moves, in_package=True) + picking.action_assign() + + selected_lines = pack1_moves.move_line_ids + pack1 = pack1_moves.move_line_ids.package_id + + move_line1, move_line2, move_line3 = selected_lines + # We'll put only product A and B in the package + move_line1.qty_done = move_line1.product_uom_qty + move_line2.qty_done = move_line2.product_uom_qty + move_line3.qty_done = 0 + + response = self.service.dispatch( + "scan_package_action", + params={ + "picking_id": picking.id, + "selected_line_ids": selected_lines.ids, + # we try to keep the goods in the same package, so we scan the + # source package but this isn't allowed as it is not a delivery + # package (i.e. having a delivery packaging set) + "barcode": pack1.name, + }, + ) + + self.assertRecordValues( + move_line1, + [{"result_package_id": pack1.id, "shopfloor_checkout_done": False}], + ) + self.assertRecordValues( + move_line2, + [{"result_package_id": pack1.id, "shopfloor_checkout_done": False}], + ) + self.assertRecordValues( + move_line3, + # qty_done was zero so it hasn't been done anyway + [{"result_package_id": pack1.id, "shopfloor_checkout_done": False}], + ) + self.assert_response( + response, + # go pack to the screen to select lines to put in packages + next_state="select_package", + data={ + "picking": self.data.picking(picking), + "selected_move_lines": self.data.move_lines(selected_lines), + "packing_info": self.service._data_for_packing_info(picking), + "allow_with_package": False, + "allow_without_package": True, + }, + message=self.service.msg_store.invalid_scanned_checkout_object_wo_package( + "package", + ppt, + ), + ) + def test_scan_package_action_scan_package_error_invalid(self): picking = self._create_picking(lines=[(self.product_a, 10)]) move = picking.move_lines @@ -215,6 +285,44 @@ def test_scan_package_action_scan_package_error_invalid(self): message=self.service.msg_store.dest_package_not_valid(other_package), ) + def test_scan_package_action_scan_package_error_invalid_package_process_type(self): + ppt = "without_package" + self.menu.sudo().package_process_type = ppt + picking = self._create_picking(lines=[(self.product_a, 10)]) + move = picking.move_lines + self._fill_stock_for_moves(move, in_package=True) + picking.action_assign() + + selected_line = move.move_line_ids + other_package = self.env["stock.quant.package"].create({}) + + response = self.service.dispatch( + "scan_package_action", + params={ + "picking_id": picking.id, + "selected_line_ids": selected_line.ids, + "barcode": other_package.name, + }, + ) + + self.assertRecordValues( + selected_line, + [ + { + "result_package_id": selected_line.package_id.id, + "shopfloor_checkout_done": False, + } + ], + ) + self._assert_selected_response( + response, + selected_line, + message=self.service.msg_store.invalid_scanned_checkout_object_wo_package( + "package", ppt + ), + allow_with_package=False, + ) + def test_scan_package_action_scan_package_use_existing_package_ok(self): picking = self._create_picking( lines=[ @@ -435,6 +543,99 @@ def test_scan_package_action_scan_packaging_bad_carrier(self): self.msg_store.goods_packed_in(selected_lines.result_package_id), ) + def test_scan_package_action_scan_packaging_invalid_package_process_type( + self, + ): + ppt = "without_package" + self.menu.sudo().package_process_type = ppt + picking = self._create_picking(lines=[(self.product_a, 10)]) + picking.carrier_id = picking.carrier_id.search([], limit=1) + pack1_moves = picking.move_lines + # put in 2 packs, for this test, we'll work on pack1 + self._fill_stock_for_moves(pack1_moves, in_package=True) + picking.action_assign() + selected_lines = pack1_moves.move_line_ids + selected_lines.qty_done = selected_lines.product_uom_qty + + packaging = ( + self.env["product.packaging"] + .sudo() + .create( + { + "name": "Product Delivery Packaging", + "product_id": selected_lines.product_id.id, + "barcode": "XXX", + "height": 12, + "width": 13, + "packaging_length": 14, + } + ) + ) + response = self.service.dispatch( + "scan_package_action", + params={ + "picking_id": picking.id, + "selected_line_ids": selected_lines.ids, + # create a new package using this packaging + "barcode": packaging.barcode, + }, + ) + self._assert_selected_response( + response, + selected_lines, + message=self.msg_store.invalid_scanned_checkout_object_wo_package( + "packaging", + ppt, + ), + allow_with_package=False, + ) + + def test_scan_package_action_scan_delivery_packaging_invalid_package_process_type( + self, + ): + ppt = "without_package" + self.menu.sudo().package_process_type = ppt + picking = self._create_picking(lines=[(self.product_a, 10)]) + picking.carrier_id = picking.carrier_id.search([], limit=1) + pack1_moves = picking.move_lines + # put in 2 packs, for this test, we'll work on pack1 + self._fill_stock_for_moves(pack1_moves, in_package=True) + picking.action_assign() + selected_lines = pack1_moves.move_line_ids + selected_lines.qty_done = selected_lines.product_uom_qty + + packaging = ( + self.env["product.packaging"] + .sudo() + .create( + { + "name": "DeliverX", + "barcode": "XXX", + "height": 12, + "width": 13, + "packaging_length": 14, + } + ) + ) + response = self.service.dispatch( + "scan_package_action", + params={ + "picking_id": picking.id, + "selected_line_ids": selected_lines.ids, + # create a new package using this packaging + "barcode": packaging.barcode, + }, + ) + self._assert_selected_response( + response, + selected_lines, + message=self.msg_store.invalid_scanned_checkout_object_wo_package( + "delivery_packaging", + ppt, + ), + allow_with_package=False, + ) + def test_scan_package_action_scan_not_found(self): picking = self._create_picking(lines=[(self.product_a, 10)]) move = picking.move_lines diff --git a/shopfloor/tests/test_checkout_select_line.py b/shopfloor/tests/test_checkout_select_line.py index 1945c7c719..63c596ee00 100644 --- a/shopfloor/tests/test_checkout_select_line.py +++ b/shopfloor/tests/test_checkout_select_line.py @@ -31,9 +31,9 @@ def test_select_line_package_ok(self): ) self._assert_selected(response, selected_lines) - def test_select_line_no_package_disabled(self): + def test_select_line_without_package(self): selected_lines = self.moves_pack.move_line_ids - self.service.work.options = {"checkout__disable_no_package": True} + self.menu.sudo().package_process_type = "without_package" response = self.service.dispatch( "select_line", params={ @@ -41,7 +41,19 @@ def test_select_line_no_package_disabled(self): "package_id": selected_lines.package_id.id, }, ) - self._assert_selected(response, selected_lines, no_package_enabled=False) + self._assert_selected(response, selected_lines, allow_with_package=False) + + def test_select_line_with_package(self): + selected_lines = self.moves_pack.move_line_ids + self.menu.sudo().package_process_type = "with_package" + response = self.service.dispatch( + "select_line", + params={ + "picking_id": self.picking.id, + "package_id": selected_lines.package_id.id, + }, + ) + self._assert_selected(response, selected_lines, allow_without_package=False) def test_select_line_move_line_package_ok(self): selected_lines = self.moves_pack.move_line_ids diff --git a/shopfloor/tests/test_checkout_select_package_base.py b/shopfloor/tests/test_checkout_select_package_base.py index a9ba7f128a..e2112f3bb7 100644 --- a/shopfloor/tests/test_checkout_select_package_base.py +++ b/shopfloor/tests/test_checkout_select_package_base.py @@ -9,7 +9,8 @@ def _assert_selected_response( selected_lines, message=None, packing_info="", - no_package_enabled=True, + allow_with_package=True, + allow_without_package=True, ): picking = selected_lines.mapped("picking_id") self.assert_response( @@ -21,7 +22,8 @@ def _assert_selected_response( ], "picking": self._picking_summary_data(picking), "packing_info": packing_info, - "no_package_enabled": no_package_enabled, + "allow_with_package": allow_with_package, + "allow_without_package": allow_without_package, }, message=message, ) diff --git a/shopfloor/views/shopfloor_menu.xml b/shopfloor/views/shopfloor_menu.xml index 291862392c..55bdc15758 100644 --- a/shopfloor/views/shopfloor_menu.xml +++ b/shopfloor/views/shopfloor_menu.xml @@ -132,6 +132,13 @@ /> + + + + diff --git a/shopfloor_mobile/static/wms/src/scenario/checkout.js b/shopfloor_mobile/static/wms/src/scenario/checkout.js index 15c2d9fb0e..2e5801816c 100644 --- a/shopfloor_mobile/static/wms/src/scenario/checkout.js +++ b/shopfloor_mobile/static/wms/src/scenario/checkout.js @@ -92,7 +92,7 @@ const Checkout = { :key="make_state_component_key(['detail-picking-select'])" />
- + Existing pack - + New pack - +