From 9342929c7236e8077d1337cdb90b6256b5b05ae2 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 21 Aug 2024 14:37:02 +0200 Subject: [PATCH 1/9] [IMP] shopflor/mobile/_packing: Allows to select package types for packing --- .../static/src/js/cluster-picking.js | 256 ++++++++++++++ shopfloor_packing/__manifest__.py | 1 + shopfloor_packing/actions/schema.py | 25 ++ shopfloor_packing/models/shopfloor_menu.py | 8 + shopfloor_packing/services/__init__.py | 2 +- shopfloor_packing/services/cluster_picking.py | 318 ++++++++++++++++-- shopfloor_packing/services/packaging.py | 75 +++++ shopfloor_packing/tests/__init__.py | 7 +- shopfloor_packing/tests/common.py | 16 + .../test_cluster_picking_pack_picking.py | 17 +- .../tests/test_cluster_picking_pick_pack.py | 104 ++++++ shopfloor_packing/views/shopfloor_menu.xml | 1 + 12 files changed, 790 insertions(+), 40 deletions(-) create mode 100644 shopfloor_packing/services/packaging.py create mode 100644 shopfloor_packing/tests/common.py create mode 100644 shopfloor_packing/tests/test_cluster_picking_pick_pack.py diff --git a/shopfloor_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js index cb9ed73a115..109d96b406e 100644 --- a/shopfloor_mobile_packing/static/src/js/cluster-picking.js +++ b/shopfloor_mobile_packing/static/src/js/cluster-picking.js @@ -20,6 +20,64 @@ ClusterPickingBase.component.template = template.replace( v-if="state_is('pack_picking_put_in_pack')" :record="state.data" /> +
+ +

+ + + +

+ + + + New pack + + + + + Process w/o pack + + +
+
+
+ + +
+ + + + + +
+
` ); @@ -34,6 +92,50 @@ ClusterPickingBase.component.computed.searchbar_input_type = function () { return "text"; }; +ClusterPickingBase.component.computed.existing_package_select_fields = function () { + return [ + {path: "weight"}, + {path: "move_line_count", label: "Line count"}, + {path: "packaging.name"}, + ]; +}; + +ClusterPickingBase.component.methods.select_package_manual_select_opts = function () { + return { + multiple: true, + initValue: this.selected_line_ids(), + card_klass: "loud-labels", + list_item_component: "picking-select-package-content", + list_item_options: {actions: ["action_qty_edit"]}, + }; +}; + +ClusterPickingBase.component.methods.select_delivery_packaging_manual_select_options = + function () { + return { + showActions: false, + }; + }; + +ClusterPickingBase.component.methods.selected_line_ids = function () { + return this.selected_lines().map(_.property("id")); +}; + +ClusterPickingBase.component.methods.selectable_lines = function () { + const stored = this.state_get_data("select_package"); + return _.result(stored, "selected_move_lines", []); +}; + +ClusterPickingBase.component.methods.selectable_line_ids = function () { + return this.selectable_lines().map(_.property("id")); +}; + +ClusterPickingBase.component.methods.selected_lines = function () { + return this.selectable_lines().filter(function (x) { + return x.qty_done > 0; + }); +}; + // Replace the data method with our new method to add // our new state let component = ClusterPickingBase.component; @@ -80,6 +182,160 @@ let data = function () { this.wait_call(this.odoo.call(endpoint, endpoint_data)); }, }; + result.states.select_package = { + // TODO: /set_line_qty is not handled yet + // because is not clear how to handle line selection + // and qty set. + // ATM given that manual-select uses v-list-item-group + // when you touch a line you select/unselect it + // which means we cannot rely on this to go to edit. + // If we need it, we have to change manual-select + // to use pure list + checkboxes. + display_info: { + title: "Select package", + scan_placeholder: "Scan existing package / package type", + }, + events: { + qty_edit: "on_qty_edit", + select: "on_select", + back: "on_back", + }, + on_scan: (scanned) => { + this.wait_call( + this.odoo.call("scan_package_action", { + picking_id: this.state.data.picking.id, + selected_line_ids: this.selectable_line_ids(), + barcode: scanned.text, + }) + ); + }, + on_select: (selected) => { + return; + // TODO: + // if (!selected) { + // return; + // } + // const orig_selected = $instance.selected_line_ids(); + // const selected_ids = selected.map(_.property("id")); + // const to_select = _.head( + // $instance.selectable_lines().filter(function (x) { + // return selected_ids.includes(x.id) && !orig_selected.includes(x.id); + // }) + // ); + // const to_unselect = _.head( + // $instance.selectable_lines().filter(function (x) { + // return !selected_ids.includes(x.id) && orig_selected.includes(x.id); + // }) + // ); + // let endpoint, move_line; + // if (to_unselect) { + // endpoint = "reset_line_qty"; + // move_line = to_unselect; + // } else if (to_select) { + // endpoint = "set_line_qty"; + // move_line = to_select; + // } + // $instance.wait_call( + // $instance.odoo.call(endpoint, { + // picking_id: $instance.state.data.picking.id, + // selected_line_ids: $instance.selectable_line_ids(), + // move_line_id: move_line.id, + // }) + // ); + }, + on_qty_edit: (record) => { + return; + // TODO: + // $instance.state_set_data( + // { + // picking: $instance.state.data.picking, + // line: record, + // selected_line_ids: $instance.selectable_line_ids(), + // }, + // "change_quantity" + // ); + // $instance.state_to("change_quantity"); + }, + on_new_pack: () => { + /** + * Trigger the call to list delivery packaging types + * as user wants to put porducts in a new pack. + */ + let endpoint, endpoint_data; + const data = this.state.data; + endpoint = "list_delivery_packaging"; + endpoint_data = { + picking_batch_id: this.current_batch().id, + picking_id: data.picking.id, + selected_line_ids: this.selectable_line_ids(), + }; + this.wait_call(this.odoo.call(endpoint, endpoint_data)); + }, + on_existing_pack: () => { + return; + // TODO: + // $instance.wait_call( + // $instance.odoo.call("list_dest_package", { + // picking_id: $instance.state.data.picking.id, + // selected_line_ids: $instance.selectable_line_ids(), + // }) + // ); + }, + on_without_pack: () => { + return; + // TODO: + // $instance.wait_call( + // $instance.odoo.call("no_package", { + // picking_id: $instance.state.data.picking.id, + // selected_line_ids: $instance.selectable_line_ids(), + // }) + // ); + }, + on_back: () => { + $instance.state_to("select_line"); + $instance.reset_notification(); + }, + }; + result.states.select_delivery_packaging = { + /** + * This will catch user events when selecting the delivery packaging type + * from: + * - the scanned barcode + * - the direct selection on screen + */ + display_info: { + title: "Select delivery packaging or scan it", + scan_placeholder: "Scan package type", + }, + events: { + select: "on_select", + back: "on_back", + }, + on_select: (selected) => { + const picking = this.current_doc().record; + const data = this.state.data.picking; + this.wait_call( + this.odoo.call("put_in_pack", { + picking_batch_id: this.current_batch().id, + picking_id: data.id, + selected_line_ids: this.selected_line_ids(), + package_type_id: selected.id, + }) + ); + }, + on_scan: (scanned) => { + const picking = this.current_doc().record; + const data = this.state.data; + this.wait_call( + this.odoo.call("scan_package_action", { + picking_id: data.id, + selected_line_ids: this.selected_line_ids(), + barcode: scanned.text, + }) + ); + }, + }; + return result; }; diff --git a/shopfloor_packing/__manifest__.py b/shopfloor_packing/__manifest__.py index 96342a8a663..32b1c8f1f42 100644 --- a/shopfloor_packing/__manifest__.py +++ b/shopfloor_packing/__manifest__.py @@ -12,6 +12,7 @@ "shopfloor", "internal_stock_quant_package", "delivery_package_type_number_parcels", + "stock_picking_delivery_package_type_domain", ], "data": ["views/shopfloor_menu.xml", "views/stock_picking.xml"], "installable": True, diff --git a/shopfloor_packing/actions/schema.py b/shopfloor_packing/actions/schema.py index a59a1685169..74cc9642bc4 100644 --- a/shopfloor_packing/actions/schema.py +++ b/shopfloor_packing/actions/schema.py @@ -12,3 +12,28 @@ def package(self, with_packaging=False): schema = super().package(with_packaging=with_packaging) schema["is_internal"] = {"required": False, "type": "boolean"} return schema + + def select_package(self) -> dict: + """ + This will return the schema expected to display the action to select the + package to put in. + """ + schema = { + "selected_move_lines": { + "type": "list", + "schema": self._schema_dict_of(self.move_line()), + }, + "picking": self._schema_dict_of(self.picking()), + "packing_info": {"type": "string", "nullable": True}, + "no_package_enabled": { + "type": "boolean", + "nullable": True, + "required": False, + }, + "package_allowed": { + "type": "boolean", + "nullable": True, + "required": False, + }, + } + return schema diff --git a/shopfloor_packing/models/shopfloor_menu.py b/shopfloor_packing/models/shopfloor_menu.py index 7786b41946c..630dd15bcf9 100644 --- a/shopfloor_packing/models/shopfloor_menu.py +++ b/shopfloor_packing/models/shopfloor_menu.py @@ -13,3 +13,11 @@ class ShopfloorMenu(models.Model): help="If you tick this box, all the picked item will be put in pack" " before the transfer.", ) + + default_pack_pickings_action = fields.Selection( + [ + ("nbr_packages", "Enter the number of packages"), + ("package_type", "Scan the package type"), + ], + default="nbr_packages", + ) diff --git a/shopfloor_packing/services/__init__.py b/shopfloor_packing/services/__init__.py index b2faa17efc3..ac4596b1320 100644 --- a/shopfloor_packing/services/__init__.py +++ b/shopfloor_packing/services/__init__.py @@ -1 +1 @@ -from . import cluster_picking +from . import cluster_picking, packaging diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 61fd381dbbd..8acde6aeb54 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -6,10 +6,121 @@ from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component +from .packaging import PackagingAction + class ClusterPicking(Component): + _inherit = "shopfloor.cluster.picking" + def _get_available_delivery_packaging(self, picking): + model = self.env["stock.package.type"] + carrier = picking.ship_carrier_id or picking.carrier_id + wizard_obj = self.env["choose.delivery.package"] + delivery_type = ( + carrier.delivery_type + if carrier.delivery_type not in ("fixed", False) + else "none" + ) + wizard = wizard_obj.with_context( + current_package_carrier_type=delivery_type + ).new({"picking_id": picking.id}) + if not carrier: + return model.browse() + return model.search( + wizard.package_type_domain, + order="number_of_parcels,name", + ) + + def list_delivery_packaging(self, picking_batch_id, picking_id, selected_line_ids): + """List available delivery packaging for given picking. + + Transitions: + * select_delivery_packaging: list available delivery packaging, the + user has to choose one to create the new package + * select_package: when no delivery packaging is available + """ + picking = self.env["stock.picking"].browse(picking_id) + message = self._check_picking_status(picking) + if message: + return self._response_for_start(message=message) + selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() + delivery_packaging = self._get_available_delivery_packaging(picking) + if not delivery_packaging: + return self._response_for_select_package( + picking, + selected_lines, + message=self.msg_store.no_delivery_packaging_available(), + ) + response = self._check_allowed_qty_done(picking, selected_lines) + if response: + return response + return self._response_for_select_delivery_packaging(picking, delivery_packaging) + + def scan_package_action(self, picking_id, selected_line_ids, barcode): + """Scan a package, a lot, a product or a package to handle a line + + When a package is scanned (only delivery ones), if the package is known + as the destination package of one of the lines or is the source package + of a selected line, the package is set to be the destination package of + all the lines to pack. + + When a product is scanned, it selects (set qty_done = reserved qty) or + deselects (set qty_done = 0) the move lines for this product. Only + products not tracked by lot can use this. + + When a lot is scanned, it does the same as for the products but based + on the lot. + + When a packaging type (one without related product) is scanned, a new + package is created and set as destination of the lines to pack. + + Lines to pack are move lines in the list of ``selected_line_ids`` + where ``qty_done`` > 0 and have not been packed yet + (``shopfloor_checkout_done is False``). + + Transitions: + * select_package: when a product or lot is scanned to select/deselect, + the client app has to show the same screen with the updated selection + * select_line: when a package or packaging type is scanned, move lines + have been put in package and we can return back to this state to handle + the other lines + * summary: if there is no other lines, go to the summary screen to be able + to close the stock picking + """ + packaging_action: PackagingAction = self._actions_for("packaging") + picking = self.env["stock.picking"].browse(picking_id) + message = self._check_picking_status(picking) + if message: + return self._response_for_select_document(message=message) + + selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() + search_result = packaging_action._scan_package_find(picking, barcode) + message = packaging_action._check_scan_package_find(picking, search_result) + if message: + return self._response_for_select_package( + picking, + selected_lines, + message=message, + ) + if search_result and search_result.type == "delivery_packaging": + package_type_id = search_result.record.id + else: + return self._response_for_select_package( + picking, + selected_lines, + message=self.msg_store.package_not_found_for_barcode(barcode), + ) + # Call the specific put in pack with package type filled in + return self._put_in_pack(self, picking, package_type_id=package_type_id) + + def _data_for_move_lines(self, lines, **kw): + return self.data.move_lines(lines, **kw) + + @property + def default_pick_pack_action(self): + return self.work.menu.default_pack_pickings_action + def _last_picked_line(self, picking): # a complete override to add a condition on internal package return fields.first( @@ -110,8 +221,14 @@ def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode): batch, ) + def _get_move_lines_to_pack(self, picking): + return picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + def _prepare_pack_picking(self, batch, message=None): picking = self._get_next_picking_to_pack(batch) + move_lines = self._get_move_lines_to_pack(picking) if not picking: return self._response_put_in_pack( batch.id, @@ -119,7 +236,13 @@ def _prepare_pack_picking(self, batch, message=None): ) if picking.is_shopfloor_packing_pack_to_scan(): return self._response_pack_picking_scan_pack(picking, message=message) - return self._response_pack_picking_put_in_pack(picking, message=message) + if self.default_pick_pack_action == "nbr_packages": + return self._response_pack_picking_put_in_pack(picking, message=message) + else: + return self._response_for_select_package( + picking, move_lines, message=message + ) + # return self._response_pack_picking_put_in_pack(picking, message=message) def prepare_unload(self, picking_batch_id): # before initializing the unloading phase we put picking in pack if @@ -131,30 +254,30 @@ def prepare_unload(self, picking_batch_id): return super().prepare_unload(picking_batch_id) return self._prepare_pack_picking(batch) - def put_in_pack(self, picking_batch_id, picking_id, nbr_packages): + def put_in_pack( + self, picking_batch_id, picking_id, nbr_packages=None, package_type_id=None + ): batch = self.env["stock.picking.batch"].browse(picking_batch_id) if not batch.exists(): return self._response_batch_does_not_exist() picking = batch.picking_ids.filtered( lambda p, picking_id=picking_id: p.id == picking_id ) - if not picking: - return self._response_put_in_pack( - picking_batch_id, - message=self.msg_store.stock_picking_not_found(), - ) - if not picking.is_shopfloor_packing_todo: - return self._response_put_in_pack( - picking_batch_id, - message=self.msg_store.stock_picking_already_packed(picking), - ) - if nbr_packages <= 0: - return self._response_put_in_pack( - picking_batch_id, - message=self.msg_store.nbr_packages_must_be_greated_than_zero(), - ) + + # Check if parameters are correct + packaging_action: PackagingAction = self._actions_for("packaging") + result = packaging_action._check_put_in_pack( + picking_batch_id, + picking, + self._response_put_in_pack, + nbr_packages=nbr_packages, + package_type_id=package_type_id, + ) + if result: + return result + savepoint = self._actions_for("savepoint").new() - pack = self._put_in_pack(picking, nbr_packages) + pack = self._put_in_pack(picking, nbr_packages, package_type_id) picking._reset_packing_packs_scanned() if not pack: savepoint.rollback() @@ -173,7 +296,7 @@ def _postprocess_put_in_pack(self, picking, pack): such as printing..""" return - def _put_in_pack(self, picking, number_of_parcels): + def _put_in_pack(self, picking, number_of_parcels=None, package_type_id=None): move_lines_to_pack = picking.move_line_ids.filtered( lambda l: l.result_package_id and l.result_package_id.is_internal ) @@ -185,15 +308,105 @@ def _put_in_pack(self, picking, number_of_parcels): ): pack = self.env["stock.quant.package"].browse(pack.get("res_id")) if isinstance(pack, self.env["stock.quant.package"].__class__): - pack.number_of_parcels = number_of_parcels + # Enhance package details either with number of packages or package_type + if number_of_parcels: + pack.number_of_parcels = number_of_parcels + elif package_type_id: + pack.package_type_id = self.env["stock.package.type"].browse( + package_type_id + ) return pack def _response_put_in_pack(self, picking_batch_id, message=None): + """ + Fallback to prepare_unload + """ res = self.prepare_unload(picking_batch_id) if message: res["message"] = message return res + def _data_for_packing_info(self, picking): + """Return the packing information + + Intended to be extended. + """ + # TODO: This could be avoided if included in the picking parser. + return "" + + def _data_response_for_select_package(self, picking, lines): + return { + "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"), + # Used by inheriting module + "package_allowed": True, + } + + def _response_for_select_package(self, picking, lines, message=None): + return self._response( + next_state="select_package", + data=self._data_response_for_select_package(picking, lines), + message=message, + ) + + def _response_for_select_dest_package(self, picking, message=None): + packages = picking.mapped("move_line_ids.result_package_id").filtered( + "package_type_id" + ) + if not packages: + # FIXME: do we want to move from 'select_dest_package' to + # 'select_package' state? Until now (before enforcing the use of + # delivery package) this part of code was never reached as we + # always had a package on the picking (source or result) + # Also the response validator did not support this state... + return self._response_for_select_package( + picking, + message=self.msg_store.no_valid_package_to_select(), + ) + picking_data = self.data.picking(picking) + packages_data = self.data.packages( + packages.with_context(picking_id=picking.id).sorted(), + picking=picking, + with_packaging=True, + with_package_move_line_count=True, + ) + return self._response( + next_state="select_dest_package", + data={ + "picking": picking_data, + "packages": packages_data, + # "selected_move_lines": self._data_for_move_lines(move_lines.sorted()), + }, + message=message, + ) + + def _data_for_delivery_packaging(self, packaging, **kw): + return self.data.delivery_packaging_list(packaging, **kw) + + def _response_for_select_delivery_packaging(self, picking, packaging, message=None): + return self._response( + next_state="select_delivery_packaging", + data={ + "picking": self.data.picking(picking), + "packaging": self._data_for_delivery_packaging(packaging), + }, + message=message, + ) + + def _check_allowed_qty_done(self, picking, lines): + for line in lines: + # Do not allow to proceed if the qty_done of + # any of the selected lines + # is higher than the quantity to do. + if line.qty_done > line.reserved_uom_qty: + return self._response_for_select_package( + picking, + lines, + message=self.msg_store.selected_lines_qty_done_higher_than_allowed(), + ) + class ShopfloorClusterPickingValidator(Component): """Validators for the Cluster Picking endpoints.""" @@ -208,7 +421,8 @@ def put_in_pack(self): "type": "integer", }, "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, - "nbr_packages": {"coerce": to_int, "required": True, "type": "integer"}, + "nbr_packages": {"coerce": to_int, "required": False, "type": "integer"}, + "package_type_id": {"coerce": to_int, "required": False, "type": "integer"}, } def scan_packing_to_pack(self): @@ -222,16 +436,44 @@ def scan_packing_to_pack(self): "barcode": {"required": True, "type": "string"}, } + def list_delivery_packaging(self) -> dict: + return { + "picking_batch_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "selected_line_ids": { + "type": "list", + "required": True, + "schema": {"coerce": to_int, "required": True, "type": "integer"}, + }, + } + + def scan_package_action(self): + return { + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "selected_line_ids": { + "type": "list", + "required": True, + "schema": {"coerce": to_int, "required": True, "type": "integer"}, + }, + "barcode": {"required": True, "type": "string"}, + } + class ShopfloorClusterPickingValidatorResponse(Component): """Validators for the Cluster Picking endpoints responses.""" _inherit = "shopfloor.cluster_picking.validator.response" - def _states(self): + def _states(self) -> dict: states = super()._states() states["pack_picking_put_in_pack"] = self.schemas_detail.pack_picking_detail() states["pack_picking_scan_pack"] = self.schemas_detail.pack_picking_detail() + states["select_package"] = self.schemas.select_package() + states["select_delivery_packaging"] = self._schema_select_delivery_packaging return states @property @@ -239,10 +481,16 @@ def _schema_pack_picking(self): schema = self.schemas_detail.pack_picking_detail() return {"type": "dict", "nullable": True, "schema": schema} + @property + def _schema_select_package(self): + schema = self.schemas.select_package() + return {"type": "dict", "nullable": True, "schema": schema} + def prepare_unload(self): res = super().prepare_unload() res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking + res["data"]["schema"]["select_package"] = self._schema_select_package return res def put_in_pack(self): @@ -252,6 +500,14 @@ def confirm_start(self): res = super().confirm_start() res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking + res["data"]["schema"]["select_package"] = self._schema_select_package + return res + + def select_package(self): + res = self._response_schema( + next_states={"select_delivery_packaging", "select_package"} + ) + res["data"]["schema"]["select_package"] = self._schema_select_package return res def scan_destination_pack(self): @@ -267,5 +523,23 @@ def scan_packing_to_pack(self): "unload_single", "pack_picking_put_in_pack", "pack_picking_scan_pack", + "select_package", } ) + + def list_delivery_packaging(self): + return self._response_schema( + next_states={"select_delivery_packaging", "select_package"} + ) + + @property + def _schema_select_delivery_packaging(self): + return { + "picking": {"type": "dict", "schema": self.schemas.picking()}, + "packaging": self.schemas._schema_list_of( + self.schemas.delivery_packaging() + ), + } + + def scan_package_action(self): + return self.select_package() diff --git a/shopfloor_packing/services/packaging.py b/shopfloor_packing/services/packaging.py new file mode 100644 index 00000000000..1094051575c --- /dev/null +++ b/shopfloor_packing/services/packaging.py @@ -0,0 +1,75 @@ +# Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com) +# Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) +# Copyright 2024 ACSONE SA/NV (https://www.acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from odoo.addons.component.core import Component +from odoo.addons.stock.models.stock_picking import Picking + + +class PackagingAction(Component): + + _inherit = "shopfloor.packaging.action" + + def _check_put_in_pack( + self, + record, + picking: Picking, + response_error_func: callable, + nbr_packages: int = None, + package_type_id: int = None, + ): + """ + This will check if parameters are correct and return a response with + the appropriate message. + """ + if not picking: + return response_error_func( + record, + message=self.msg_store.stock_picking_not_found(), + ) + if not picking.is_shopfloor_packing_todo: + return response_error_func( + record, + message=self.msg_store.stock_picking_already_packed(picking), + ) + if isinstance(nbr_packages, int) and nbr_packages <= 0: + return response_error_func( + record, + message=self.msg_store.nbr_packages_must_be_greated_than_zero(), + ) + # Check if package type exists + if package_type_id and not nbr_packages: + package_type = ( + self.env["stock.package.type"].browse(package_type_id).exists() + ) + if not package_type: + return response_error_func( + record, + message=self.msg_store.record_not_found(), + ) + return False + + def _scan_package_find(self, picking, barcode, search_types=None): + search = self._actions_for("search") + search_types = ( + "package", + "product", + "packaging", + "lot", + "serial", + "delivery_packaging", + ) + return search.find( + barcode, + types=search_types, + handler_kw=dict( + lot=dict(products=picking.move_ids.product_id), + serial=dict(products=picking.move_ids.product_id), + ), + ) + + def _check_scan_package_find(self, picking, search_result): + # Used by inheriting modules + return False diff --git a/shopfloor_packing/tests/__init__.py b/shopfloor_packing/tests/__init__.py index e84b11d98c7..f4e45effd31 100644 --- a/shopfloor_packing/tests/__init__.py +++ b/shopfloor_packing/tests/__init__.py @@ -1,2 +1,5 @@ -from . import test_cluster_picking_pack_picking -from . import test_cluster_picking_unload +from . import ( # test_cluster_picking_pick_pack, + common, + test_cluster_picking_pack_picking, + test_cluster_picking_unload, +) diff --git a/shopfloor_packing/tests/common.py b/shopfloor_packing/tests/common.py new file mode 100644 index 00000000000..0655d670229 --- /dev/null +++ b/shopfloor_packing/tests/common.py @@ -0,0 +1,16 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.shopfloor.tests.test_cluster_picking_unload import ( + ClusterPickingUnloadingCommonCase, +) + + +# pylint: disable=missing-return +class ClusterPickingUnloadPackingCommonCase(ClusterPickingUnloadingCommonCase): + @classmethod + def setUpClassBaseData(cls, *args, **kwargs): + super().setUpClassBaseData(*args, **kwargs) + cls.bin1.write({"name": "bin1", "is_internal": True}) + cls.bin2.write({"name": "bin2", "is_internal": True}) + cls.menu.sudo().pack_pickings = True diff --git a/shopfloor_packing/tests/test_cluster_picking_pack_picking.py b/shopfloor_packing/tests/test_cluster_picking_pack_picking.py index 22a44f761b7..1be91709e04 100644 --- a/shopfloor_packing/tests/test_cluster_picking_pack_picking.py +++ b/shopfloor_packing/tests/test_cluster_picking_pack_picking.py @@ -1,19 +1,6 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo.addons.shopfloor.tests.test_cluster_picking_unload import ( - ClusterPickingUnloadingCommonCase, -) - - -# pylint: disable=missing-return -class ClusterPickingUnloadPackingCommonCase(ClusterPickingUnloadingCommonCase): - @classmethod - def setUpClassBaseData(cls, *args, **kwargs): - super().setUpClassBaseData(*args, **kwargs) - cls.bin1.write({"name": "bin1", "is_internal": True}) - cls.bin2.write({"name": "bin2", "is_internal": True}) - cls.menu.sudo().pack_pickings = True +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from .common import ClusterPickingUnloadPackingCommonCase class TestClusterPickingPrepareUnload(ClusterPickingUnloadPackingCommonCase): diff --git a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py new file mode 100644 index 00000000000..e35d7975f4e --- /dev/null +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -0,0 +1,104 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from .common import ClusterPickingUnloadPackingCommonCase + + +class TestClusterPickingPrepareUnload(ClusterPickingUnloadPackingCommonCase): + def test_prepare_unload_all_same_dest_with_dest_package(self): + """ + Activate the behavior that allows to pack at the pick step (cluster) + Activate the behavior that change the default action -> Scan the package type + At the unload step, ask to select a delivery package (from types) + """ + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + location = self.packing_location + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 4, + }, + ) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 4) + + picking = move_lines[0].picking_id + data = self.data_detail.pack_picking_detail(picking) + # message = self.service.msg_store.stock_picking_packed_successfully(picking) + self.assert_response( + response, next_state="pack_picking_scan_pack", data=data, message=message + ) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin2.name, + }, + ) + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_put_in_pack", + data=data, + ) + # we process to the put in pack + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "nbr_packages": 2, + }, + ) + data = self._data_for_batch(self.batch, location) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + self.assert_response( + response, next_state="unload_all", data=data, message=message + ) + + result_package = picking.move_line_ids.mapped("result_package_id") + self.assertEqual(len(result_package), 1) + self.assertEqual(result_package[0].number_of_parcels, 2) diff --git a/shopfloor_packing/views/shopfloor_menu.xml b/shopfloor_packing/views/shopfloor_menu.xml index d24718b4c04..d251ba72d14 100644 --- a/shopfloor_packing/views/shopfloor_menu.xml +++ b/shopfloor_packing/views/shopfloor_menu.xml @@ -14,6 +14,7 @@ attrs="{'invisible': [('scenario', '!=', 'cluster_picking')]}" > + From bf53107c1ad51e4e61fbf8dfc4aafb17c83c3737 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Fri, 20 Sep 2024 16:44:01 +0200 Subject: [PATCH 2/9] [IMP] shopfloor_packing: Move data retrieval to data action + enable tests --- shopfloor_packing/actions/data.py | 18 ++++++++++++++++++ shopfloor_packing/services/cluster_picking.py | 15 +-------------- shopfloor_packing/tests/__init__.py | 4 ++-- .../tests/test_cluster_picking_pick_pack.py | 15 +++++++++++---- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/shopfloor_packing/actions/data.py b/shopfloor_packing/actions/data.py index e8d036e7c9e..dc36ffabb74 100644 --- a/shopfloor_packing/actions/data.py +++ b/shopfloor_packing/actions/data.py @@ -12,3 +12,21 @@ def _package_parser(self): res = super()._package_parser res.append("is_internal") return res + + def _data_for_packing_info(self, picking): + """Return the packing information + + Intended to be extended. + """ + # TODO: This could be avoided if included in the picking parser. + return "" + + def select_package(self, picking, lines): + return { + "selected_move_lines": self.move_lines(lines.sorted()), + "picking": self.picking(picking), + "packing_info": self._data_for_packing_info(picking), + # "no_package_enabled": not self.options.get("checkout__disable_no_package"), + # Used by inheriting module + "package_allowed": True, + } diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 8acde6aeb54..c32b2d131d5 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -114,9 +114,6 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode): # Call the specific put in pack with package type filled in return self._put_in_pack(self, picking, package_type_id=package_type_id) - def _data_for_move_lines(self, lines, **kw): - return self.data.move_lines(lines, **kw) - @property def default_pick_pack_action(self): return self.work.menu.default_pack_pickings_action @@ -334,20 +331,10 @@ def _data_for_packing_info(self, picking): # TODO: This could be avoided if included in the picking parser. return "" - def _data_response_for_select_package(self, picking, lines): - return { - "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"), - # Used by inheriting module - "package_allowed": True, - } - def _response_for_select_package(self, picking, lines, message=None): return self._response( next_state="select_package", - data=self._data_response_for_select_package(picking, lines), + data=self.data.select_package(picking, lines), message=message, ) diff --git a/shopfloor_packing/tests/__init__.py b/shopfloor_packing/tests/__init__.py index f4e45effd31..b3db59427f7 100644 --- a/shopfloor_packing/tests/__init__.py +++ b/shopfloor_packing/tests/__init__.py @@ -1,5 +1,5 @@ -from . import ( # test_cluster_picking_pick_pack, - common, +from . import ( test_cluster_picking_pack_picking, + test_cluster_picking_pick_pack, test_cluster_picking_unload, ) diff --git a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py index e35d7975f4e..0eb3ee893f3 100644 --- a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -1,4 +1,4 @@ -# Copyright 2021 ACSONE SA/NV +# Copyright 2024 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from .common import ClusterPickingUnloadPackingCommonCase @@ -34,6 +34,9 @@ def test_prepare_unload_all_same_dest_with_dest_package(self): next_state="pack_picking_scan_pack", data=data, ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) # we scan the pack response = self.service.dispatch( "scan_packing_to_pack", @@ -43,7 +46,8 @@ def test_prepare_unload_all_same_dest_with_dest_package(self): "barcode": self.bin1.name, }, ) - data = self.data_detail.pack_picking_detail(picking) + + data = self.data.select_package(picking, lines) self.assert_response( response, next_state="select_package", @@ -69,6 +73,9 @@ def test_prepare_unload_all_same_dest_with_dest_package(self): self.assert_response( response, next_state="pack_picking_scan_pack", data=data, message=message ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) # we scan the pack response = self.service.dispatch( "scan_packing_to_pack", @@ -78,10 +85,10 @@ def test_prepare_unload_all_same_dest_with_dest_package(self): "barcode": self.bin2.name, }, ) - data = self.data_detail.pack_picking_detail(picking) + data = self.data.select_package(picking, lines) self.assert_response( response, - next_state="pack_picking_put_in_pack", + next_state="select_package", data=data, ) # we process to the put in pack From 988b1531703b37fb32e37eabb2b19e7a6d69aa10 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 29 Oct 2024 13:53:32 +0100 Subject: [PATCH 3/9] [DONT MERGE] test-requirements.txt --- test-requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index 0d19f488581..5c53a5e2f50 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,7 @@ odoo_test_helper # stock-logistics-tracking odoo-addon-internal-stock-quant-package @ git+https://github.com/OCA/stock-logistics-tracking.git@refs/pull/34/head#subdirectory=setup/internal_stock_quant_package + +odoo-addon-stock-picking-delivery-package-type-domain @ git+https://github.com/OCA/delivery-carrier.git@refs/pull/846/head#subdirectory=setup/stock_picking_delivery_package_type_domain + +odoo-addon-shopfloor @ git+https://github.com/OCA/wms.git@refs/pull/942/head#subdirectory=setup/shopfloor From 11fc8dc0de3999ffb31f0465417b92593813d985 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 29 Oct 2024 14:09:48 +0100 Subject: [PATCH 4/9] [IMP] shopfloor_packing: Use better message --- shopfloor_packing/services/cluster_picking.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index c32b2d131d5..0909fb2ba90 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -391,7 +391,9 @@ def _check_allowed_qty_done(self, picking, lines): return self._response_for_select_package( picking, lines, - message=self.msg_store.selected_lines_qty_done_higher_than_allowed(), + message=self.msg_store.selected_lines_qty_done_higher_than_allowed( + line + ), ) From a4eda71cd4f3bcb561dbefae8d354fd6d0929719 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 16 Jan 2025 17:58:23 +0100 Subject: [PATCH 5/9] [IMP] shopfloor_packing: Use appropriate name for packing action + improve tests --- shopfloor_packing/services/__init__.py | 2 +- shopfloor_packing/services/cluster_picking.py | 16 +- .../services/{packaging.py => packing.py} | 7 +- .../static/description/index.html | 11 +- .../tests/test_cluster_picking_pick_pack.py | 239 ++++++++++++++++++ 5 files changed, 260 insertions(+), 15 deletions(-) rename shopfloor_packing/services/{packaging.py => packing.py} (92%) diff --git a/shopfloor_packing/services/__init__.py b/shopfloor_packing/services/__init__.py index ac4596b1320..265d3f60038 100644 --- a/shopfloor_packing/services/__init__.py +++ b/shopfloor_packing/services/__init__.py @@ -1 +1 @@ -from . import cluster_picking, packaging +from . import cluster_picking, packing diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 0909fb2ba90..3f7fe36cb0a 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -6,14 +6,14 @@ from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component -from .packaging import PackagingAction +from .packing import PackingAction class ClusterPicking(Component): _inherit = "shopfloor.cluster.picking" - def _get_available_delivery_packaging(self, picking): + def _get_available_delivery_package_type(self, picking): model = self.env["stock.package.type"] carrier = picking.ship_carrier_id or picking.carrier_id wizard_obj = self.env["choose.delivery.package"] @@ -45,7 +45,7 @@ def list_delivery_packaging(self, picking_batch_id, picking_id, selected_line_id if message: return self._response_for_start(message=message) selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() - delivery_packaging = self._get_available_delivery_packaging(picking) + delivery_packaging = self._get_available_delivery_package_type(picking) if not delivery_packaging: return self._response_for_select_package( picking, @@ -88,15 +88,15 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode): * summary: if there is no other lines, go to the summary screen to be able to close the stock picking """ - packaging_action: PackagingAction = self._actions_for("packaging") + packing_action: PackingAction = self._actions_for("packing") picking = self.env["stock.picking"].browse(picking_id) message = self._check_picking_status(picking) if message: return self._response_for_select_document(message=message) selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists() - search_result = packaging_action._scan_package_find(picking, barcode) - message = packaging_action._check_scan_package_find(picking, search_result) + search_result = packing_action._scan_package_find(picking, barcode) + message = packing_action._check_scan_package_find(picking, search_result) if message: return self._response_for_select_package( picking, @@ -262,8 +262,8 @@ def put_in_pack( ) # Check if parameters are correct - packaging_action: PackagingAction = self._actions_for("packaging") - result = packaging_action._check_put_in_pack( + packing_action: PackingAction = self._actions_for("packing") + result = packing_action._check_put_in_pack( picking_batch_id, picking, self._response_put_in_pack, diff --git a/shopfloor_packing/services/packaging.py b/shopfloor_packing/services/packing.py similarity index 92% rename from shopfloor_packing/services/packaging.py rename to shopfloor_packing/services/packing.py index 1094051575c..66748872d29 100644 --- a/shopfloor_packing/services/packaging.py +++ b/shopfloor_packing/services/packing.py @@ -8,9 +8,12 @@ from odoo.addons.stock.models.stock_picking import Picking -class PackagingAction(Component): +class PackingAction(Component): - _inherit = "shopfloor.packaging.action" + _name = "shopfloor.packing.action" + _inherit = "shopfloor.process.action" + _usage = "packing" + _description = "This is the service to put products in pack" def _check_put_in_pack( self, diff --git a/shopfloor_packing/static/description/index.html b/shopfloor_packing/static/description/index.html index 105276390d1..3217e6afbd8 100644 --- a/shopfloor_packing/static/description/index.html +++ b/shopfloor_packing/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -420,7 +421,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py index 0eb3ee893f3..1f166bc3a0a 100644 --- a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -109,3 +109,242 @@ def test_prepare_unload_all_same_dest_with_dest_package(self): result_package = picking.move_line_ids.mapped("result_package_id") self.assertEqual(len(result_package), 1) self.assertEqual(result_package[0].number_of_parcels, 2) + + def test_pack_no_package_type(self): + """ + Activate the behavior that allows to pack at the pick step (cluster) + Activate the behavior that change the default action -> Scan the package type + At the unload step, ask to select a delivery package (from types) + """ + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + # We use new pack + response = self.service.dispatch( + "list_delivery_packaging", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "selected_line_ids": lines.ids, + }, + ) + message = { + "message_type": "warning", + "body": "No delivery package type available.", + } + data = self.data.select_package(picking, lines) + self.assert_response( + response, next_state="select_package", data=data, message=message + ) + + def test_list_delivery_package_picking_done(self): + """ """ + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + + picking._action_done() + # Delivery is already done + response = self.service.dispatch( + "list_delivery_packaging", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "selected_line_ids": lines.ids, + }, + ) + message = {"message_type": "info", "body": "Operation already processed."} + data = {} + self.assert_response(response, next_state="start", data=data, message=message) + + def test_pack_package_type(self): + self.carrier_product = ( + self.env["product.product"] + .sudo() + .create( + { + "name": "Test Product", + "type": "service", + } + ) + ) + self.carrier = ( + self.env["delivery.carrier"] + .sudo() + .create( + { + "name": "Test Carrier", + "product_id": self.carrier_product.id, + } + ) + ) + self.package_type = ( + self.env["stock.package.type"] + .sudo() + .create( + { + "name": "BOX-5", + "package_carrier_type": "none", + "number_of_parcels": 5.0, + } + ) + ) + self.package_types = self.env["stock.package.type"].search( + [("package_carrier_type", "=", "none")], order="number_of_parcels,name" + ) + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + picking.carrier_id = self.carrier + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + # We use new pack + response = self.service.dispatch( + "list_delivery_packaging", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "selected_line_ids": lines.ids, + }, + ) + data = {} + data["packaging"] = self.data.delivery_packaging_list(self.package_types) + data["picking"] = self.data.picking(picking) + self.assert_response( + response, + next_state="select_delivery_packaging", + data=data, + ) + + response = self.service.dispatch( + "put_in_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "package_type_id": self.package_type.id, + }, + ) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + next_picking = self.batch.picking_ids.filtered( + lambda p: p.is_shopfloor_packing_todo + ) + data = data = self.data_detail.pack_picking_detail(next_picking) + + self.assert_response( + response, next_state="pack_picking_scan_pack", data=data, message=message + ) From 83c56c1cf7d6455b0a21fbb6429de87270473105 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 21 Jan 2025 11:28:09 +0100 Subject: [PATCH 6/9] [IMP] shopfloor_packing: Several cosmetics + update list_delivery_packaging endpoint + add tests --- shopfloor_packing/services/cluster_picking.py | 322 ++++++++++-------- .../tests/test_cluster_picking_pick_pack.py | 274 +++++++++++++-- 2 files changed, 419 insertions(+), 177 deletions(-) diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 3f7fe36cb0a..5c3cd5978d5 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -1,10 +1,14 @@ # Copyright 2021 ACSONE SA/NV (https://www.acsone.eu) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component +from odoo.addons.stock.models.stock_move_line import StockMoveLine +from odoo.addons.stock.models.stock_package_type import PackageType +from odoo.addons.stock.models.stock_picking import Picking +from odoo.addons.stock.models.stock_quant import QuantPackage from .packing import PackingAction @@ -13,32 +17,17 @@ class ClusterPicking(Component): _inherit = "shopfloor.cluster.picking" - def _get_available_delivery_package_type(self, picking): - model = self.env["stock.package.type"] - carrier = picking.ship_carrier_id or picking.carrier_id - wizard_obj = self.env["choose.delivery.package"] - delivery_type = ( - carrier.delivery_type - if carrier.delivery_type not in ("fixed", False) - else "none" - ) - wizard = wizard_obj.with_context( - current_package_carrier_type=delivery_type - ).new({"picking_id": picking.id}) - if not carrier: - return model.browse() - return model.search( - wizard.package_type_domain, - order="number_of_parcels,name", - ) + # PUBLIC METHODS - ENDPOINTS - def list_delivery_packaging(self, picking_batch_id, picking_id, selected_line_ids): - """List available delivery packaging for given picking. + def list_delivery_package_types( + self, picking_batch_id, picking_id, selected_line_ids + ) -> dict: + """List available delivery package types for given picking. Transitions: - * select_delivery_packaging: list available delivery packaging, the + * select_delivery_package_types: list available delivery package types, the user has to choose one to create the new package - * select_package: when no delivery packaging is available + * select_package: when no delivery package types are available """ picking = self.env["stock.picking"].browse(picking_id) message = self._check_picking_status(picking) @@ -55,9 +44,11 @@ def list_delivery_packaging(self, picking_batch_id, picking_id, selected_line_id response = self._check_allowed_qty_done(picking, selected_lines) if response: return response - return self._response_for_select_delivery_packaging(picking, delivery_packaging) + return self._response_for_select_delivery_package_type( + picking, delivery_packaging + ) - def scan_package_action(self, picking_id, selected_line_ids, barcode): + def scan_package_action(self, picking_id, selected_line_ids, barcode) -> dict: """Scan a package, a lot, a product or a package to handle a line When a package is scanned (only delivery ones), if the package is known @@ -112,54 +103,13 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode): message=self.msg_store.package_not_found_for_barcode(barcode), ) # Call the specific put in pack with package type filled in - return self._put_in_pack(self, picking, package_type_id=package_type_id) - - @property - def default_pick_pack_action(self): - return self.work.menu.default_pack_pickings_action - - def _last_picked_line(self, picking): - # a complete override to add a condition on internal package - return fields.first( - picking.move_line_ids.filtered( - lambda l: l.qty_done > 0 - and l.result_package_id.is_internal - # if we are moving the entire package, we shouldn't - # add stuff inside it, it's not a new package - and l.package_id != l.result_package_id - ).sorted(key="write_date", reverse=True) - ) - - def _get_next_picking_to_pack(self, batch): - """ - Return a picking not yet packed. - - The returned picking is the first - one into the list of picking not yet packed (is_shopfloor_packing_todo=True). - nbr_packages - """ - pickings_to_pack = batch.picking_ids.filtered( - lambda p: p.is_shopfloor_packing_todo + return self.put_in_pack( + picking.batch_id.id, picking.id, package_type_id=package_type_id ) - move_lines = pickings_to_pack.mapped("move_line_ids") - move_lines = move_lines.filtered( - lambda ml: ml.result_package_id.is_internal - ).sorted(key=lambda ml: ml.result_package_id.name) - return move_lines[0].picking_id if move_lines else move_lines.picking_id - def _response_pack_picking_put_in_pack(self, picking, message=None): - data = self.data_detail.pack_picking_detail(picking) - return self._response( - next_state="pack_picking_put_in_pack", data=data, message=message - ) - - def _response_pack_picking_scan_pack(self, picking, message=None): - data = self.data_detail.pack_picking_detail(picking) - return self._response( - next_state="pack_picking_scan_pack", data=data, message=message - ) - - def scan_destination_pack(self, picking_batch_id, move_line_id, barcode, quantity): + def scan_destination_pack( + self, picking_batch_id, move_line_id, barcode, quantity + ) -> dict: search = self._actions_for("search") bin_package = search.package_from_scan(barcode) @@ -179,7 +129,7 @@ def scan_destination_pack(self, picking_batch_id, move_line_id, barcode, quantit picking_batch_id, move_line_id, barcode, quantity ) - def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode): + def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode) -> dict: batch = self.env["stock.picking.batch"].browse(picking_batch_id) if not batch.exists(): return self._response_batch_does_not_exist() @@ -218,30 +168,7 @@ def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode): batch, ) - def _get_move_lines_to_pack(self, picking): - return picking.move_line_ids.filtered( - lambda ml: ml.result_package_id.is_internal - ).sorted(key=lambda ml: ml.result_package_id.name) - - def _prepare_pack_picking(self, batch, message=None): - picking = self._get_next_picking_to_pack(batch) - move_lines = self._get_move_lines_to_pack(picking) - if not picking: - return self._response_put_in_pack( - batch.id, - message=self.msg_store.stock_picking_packed_successfully(picking), - ) - if picking.is_shopfloor_packing_pack_to_scan(): - return self._response_pack_picking_scan_pack(picking, message=message) - if self.default_pick_pack_action == "nbr_packages": - return self._response_pack_picking_put_in_pack(picking, message=message) - else: - return self._response_for_select_package( - picking, move_lines, message=message - ) - # return self._response_pack_picking_put_in_pack(picking, message=message) - - def prepare_unload(self, picking_batch_id): + def prepare_unload(self, picking_batch_id) -> dict: # before initializing the unloading phase we put picking in pack if # required by the scenario batch = self.env["stock.picking.batch"].browse(picking_batch_id) @@ -253,7 +180,7 @@ def prepare_unload(self, picking_batch_id): def put_in_pack( self, picking_batch_id, picking_id, nbr_packages=None, package_type_id=None - ): + ) -> dict: batch = self.env["stock.picking.batch"].browse(picking_batch_id) if not batch.exists(): return self._response_batch_does_not_exist() @@ -288,12 +215,105 @@ def put_in_pack( message=self.msg_store.stock_picking_packed_successfully(picking), ) + # HELPER METHODS + + @property + def default_pick_pack_action(self): + return self.work.menu.default_pack_pickings_action + + def _get_available_delivery_package_type(self, picking) -> PackageType: + """ + This returns available packages types for the carrier if defined + or package types that have "none" delivery type. + + The returned package types are ordered by number of parcels then + by name. + """ + model = self.env["stock.package.type"] + carrier = picking.ship_carrier_id or picking.carrier_id + wizard_obj = self.env["choose.delivery.package"] + delivery_type = ( + carrier.delivery_type + if carrier.delivery_type not in ("fixed", False) + else "none" + ) + wizard = wizard_obj.with_context( + current_package_carrier_type=delivery_type + ).new({"picking_id": picking.id}) + if not carrier: + return model.browse() + return model.search( + wizard.package_type_domain, + order="number_of_parcels,name", + ) + + def _last_picked_line(self, picking) -> StockMoveLine: + # a complete override to add a condition on internal package + # TODO: Add a hook to avoid re-writing this + return fields.first( + picking.move_line_ids.filtered( + lambda l: l.qty_done > 0 + and l.result_package_id.is_internal + # if we are moving the entire package, we shouldn't + # add stuff inside it, it's not a new package + and l.package_id != l.result_package_id + ).sorted(key="write_date", reverse=True) + ) + + def _get_next_picking_to_pack(self, batch) -> Picking: + """ + Return a picking not yet packed. + + The returned picking is the first + one into the list of picking not yet packed (is_shopfloor_packing_todo=True). + nbr_packages + """ + pickings_to_pack = batch.picking_ids.filtered( + lambda p: p.is_shopfloor_packing_todo + ) + move_lines = pickings_to_pack.mapped("move_line_ids") + move_lines = move_lines.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + return fields.first(move_lines).picking_id + + def _get_move_lines_to_pack(self, picking) -> StockMoveLine: + """ + This returns the lines that have an internal package + """ + return picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + + def _prepare_pack_picking(self, batch, message=None) -> dict: + picking = self._get_next_picking_to_pack(batch) + move_lines = self._get_move_lines_to_pack(picking) + if not picking: + return self._response_put_in_pack( + batch.id, + message=self.msg_store.stock_picking_packed_successfully(picking), + ) + if picking.is_shopfloor_packing_pack_to_scan(): + return self._response_pack_picking_scan_pack(picking, message=message) + if self.default_pick_pack_action == "nbr_packages": + return self._response_pack_picking_put_in_pack(picking, message=message) + else: + return self._response_for_select_package( + picking, move_lines, message=message + ) + def _postprocess_put_in_pack(self, picking, pack): """Override this method to include post-processing logic for the new package, such as printing..""" return - def _put_in_pack(self, picking, number_of_parcels=None, package_type_id=None): + def _put_in_pack( + self, picking, number_of_parcels=None, package_type_id=None + ) -> QuantPackage: + """ + This will enhance the put in pack flow by adding the number + of parcels or the package type to the generated package. + """ move_lines_to_pack = picking.move_line_ids.filtered( lambda l: l.result_package_id and l.result_package_id.is_internal ) @@ -314,15 +334,6 @@ def _put_in_pack(self, picking, number_of_parcels=None, package_type_id=None): ) return pack - def _response_put_in_pack(self, picking_batch_id, message=None): - """ - Fallback to prepare_unload - """ - res = self.prepare_unload(picking_batch_id) - if message: - res["message"] = message - return res - def _data_for_packing_info(self, picking): """Return the packing information @@ -331,14 +342,45 @@ def _data_for_packing_info(self, picking): # TODO: This could be avoided if included in the picking parser. return "" - def _response_for_select_package(self, picking, lines, message=None): + def _data_for_delivery_package_type(self, packaging, **kw): + return self.data.delivery_packaging_list(packaging, **kw) + + def _check_allowed_qty_done(self, picking, lines) -> dict: + for line in lines: + # Do not allow to proceed if the qty_done of + # any of the selected lines + # is higher than the quantity to do. + if line.qty_done > line.reserved_uom_qty: + return self._response_for_select_package( + picking, + lines, + message=self.msg_store.selected_lines_qty_done_higher_than_allowed( + line + ), + ) + + # RESPONSES + + def _response_pack_picking_put_in_pack(self, picking, message=None) -> dict: + data = self.data_detail.pack_picking_detail(picking) + return self._response( + next_state="pack_picking_put_in_pack", data=data, message=message + ) + + def _response_pack_picking_scan_pack(self, picking, message=None) -> dict: + data = self.data_detail.pack_picking_detail(picking) + return self._response( + next_state="pack_picking_scan_pack", data=data, message=message + ) + + def _response_for_select_package(self, picking, lines, message=None) -> dict: return self._response( next_state="select_package", data=self.data.select_package(picking, lines), message=message, ) - def _response_for_select_dest_package(self, picking, message=None): + def _response_for_select_dest_package(self, picking, message=None) -> dict: packages = picking.mapped("move_line_ids.result_package_id").filtered( "package_type_id" ) @@ -369,32 +411,26 @@ def _response_for_select_dest_package(self, picking, message=None): message=message, ) - def _data_for_delivery_packaging(self, packaging, **kw): - return self.data.delivery_packaging_list(packaging, **kw) - - def _response_for_select_delivery_packaging(self, picking, packaging, message=None): + def _response_for_select_delivery_package_type( + self, picking, packaging, message=None + ) -> dict: return self._response( next_state="select_delivery_packaging", data={ "picking": self.data.picking(picking), - "packaging": self._data_for_delivery_packaging(packaging), + "packaging": self._data_for_delivery_package_type(packaging), }, message=message, ) - def _check_allowed_qty_done(self, picking, lines): - for line in lines: - # Do not allow to proceed if the qty_done of - # any of the selected lines - # is higher than the quantity to do. - if line.qty_done > line.reserved_uom_qty: - return self._response_for_select_package( - picking, - lines, - message=self.msg_store.selected_lines_qty_done_higher_than_allowed( - line - ), - ) + def _response_put_in_pack(self, picking_batch_id, message=None) -> dict: + """ + Fallback to prepare_unload + """ + res = self.prepare_unload(picking_batch_id) + if message: + res["message"] = message + return res class ShopfloorClusterPickingValidator(Component): @@ -402,7 +438,7 @@ class ShopfloorClusterPickingValidator(Component): _inherit = "shopfloor.cluster_picking.validator" - def put_in_pack(self): + def put_in_pack(self) -> dict: return { "picking_batch_id": { "coerce": to_int, @@ -414,7 +450,7 @@ def put_in_pack(self): "package_type_id": {"coerce": to_int, "required": False, "type": "integer"}, } - def scan_packing_to_pack(self): + def scan_packing_to_pack(self) -> dict: return { "picking_batch_id": { "coerce": to_int, @@ -425,7 +461,7 @@ def scan_packing_to_pack(self): "barcode": {"required": True, "type": "string"}, } - def list_delivery_packaging(self) -> dict: + def list_delivery_package_types(self) -> dict: return { "picking_batch_id": { "coerce": to_int, @@ -440,7 +476,7 @@ def list_delivery_packaging(self) -> dict: }, } - def scan_package_action(self): + def scan_package_action(self) -> dict: return { "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, "selected_line_ids": { @@ -466,46 +502,46 @@ def _states(self) -> dict: return states @property - def _schema_pack_picking(self): + def _schema_pack_picking(self) -> dict: schema = self.schemas_detail.pack_picking_detail() return {"type": "dict", "nullable": True, "schema": schema} @property - def _schema_select_package(self): + def _schema_select_package(self) -> dict: schema = self.schemas.select_package() return {"type": "dict", "nullable": True, "schema": schema} - def prepare_unload(self): + def prepare_unload(self) -> dict: res = super().prepare_unload() res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking res["data"]["schema"]["select_package"] = self._schema_select_package return res - def put_in_pack(self): + def put_in_pack(self) -> dict: return self.prepare_unload() - def confirm_start(self): + def confirm_start(self) -> dict: res = super().confirm_start() res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking res["data"]["schema"]["select_package"] = self._schema_select_package return res - def select_package(self): + def select_package(self) -> dict: res = self._response_schema( next_states={"select_delivery_packaging", "select_package"} ) res["data"]["schema"]["select_package"] = self._schema_select_package return res - def scan_destination_pack(self): + def scan_destination_pack(self) -> dict: res = super().scan_destination_pack() res["data"]["schema"]["pack_picking_put_in_pack"] = self._schema_pack_picking res["data"]["schema"]["pack_picking_scan_pack"] = self._schema_pack_picking return res - def scan_packing_to_pack(self): + def scan_packing_to_pack(self) -> dict: return self._response_schema( next_states={ "unload_all", @@ -516,13 +552,13 @@ def scan_packing_to_pack(self): } ) - def list_delivery_packaging(self): + def list_delivery_package_types(self) -> dict: return self._response_schema( next_states={"select_delivery_packaging", "select_package"} ) @property - def _schema_select_delivery_packaging(self): + def _schema_select_delivery_packaging(self) -> dict: return { "picking": {"type": "dict", "schema": self.schemas.picking()}, "packaging": self.schemas._schema_list_of( @@ -530,5 +566,5 @@ def _schema_select_delivery_packaging(self): ), } - def scan_package_action(self): - return self.select_package() + def scan_package_action(self) -> dict: + return self.prepare_unload() diff --git a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py index 1f166bc3a0a..ad256cc31ea 100644 --- a/shopfloor_packing/tests/test_cluster_picking_pick_pack.py +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -1,9 +1,48 @@ # Copyright 2024 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.fields import first + from .common import ClusterPickingUnloadPackingCommonCase class TestClusterPickingPrepareUnload(ClusterPickingUnloadPackingCommonCase): + def _create_package_type(self): + self.carrier_product = ( + self.env["product.product"] + .sudo() + .create( + { + "name": "Test Product", + "type": "service", + } + ) + ) + self.carrier = ( + self.env["delivery.carrier"] + .sudo() + .create( + { + "name": "Test Carrier", + "product_id": self.carrier_product.id, + } + ) + ) + self.package_type = ( + self.env["stock.package.type"] + .sudo() + .create( + { + "name": "BOX-5", + "package_carrier_type": "none", + "number_of_parcels": 5.0, + "barcode": "BOX-5", + } + ) + ) + self.package_types = self.env["stock.package.type"].search( + [("package_carrier_type", "=", "none")], order="number_of_parcels,name" + ) + def test_prepare_unload_all_same_dest_with_dest_package(self): """ Activate the behavior that allows to pack at the pick step (cluster) @@ -160,7 +199,7 @@ def test_pack_no_package_type(self): ) # We use new pack response = self.service.dispatch( - "list_delivery_packaging", + "list_delivery_package_types", params={ "picking_batch_id": self.batch.id, "picking_id": picking.id, @@ -224,7 +263,7 @@ def test_list_delivery_package_picking_done(self): picking._action_done() # Delivery is already done response = self.service.dispatch( - "list_delivery_packaging", + "list_delivery_package_types", params={ "picking_batch_id": self.batch.id, "picking_id": picking.id, @@ -235,41 +274,79 @@ def test_list_delivery_package_picking_done(self): data = {} self.assert_response(response, next_state="start", data=data, message=message) - def test_pack_package_type(self): - self.carrier_product = ( - self.env["product.product"] - .sudo() - .create( - { - "name": "Test Product", - "type": "service", - } - ) + def test_list_delivery_package_picking_qty_superior(self): + self._create_package_type() + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } ) - self.carrier = ( - self.env["delivery.carrier"] - .sudo() - .create( - { - "name": "Test Carrier", - "product_id": self.carrier_product.id, - } - ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} ) - self.package_type = ( - self.env["stock.package.type"] - .sudo() - .create( - { - "name": "BOX-5", - "package_carrier_type": "none", - "number_of_parcels": 5.0, - } - ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + picking.carrier_id = self.carrier + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, ) - self.package_types = self.env["stock.package.type"].search( - [("package_carrier_type", "=", "none")], order="number_of_parcels,name" + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, ) + line = first(picking.move_line_ids) + line.qty_done = line.reserved_qty + 1 + # Delivery is already done + response = self.service.dispatch( + "list_delivery_package_types", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "selected_line_ids": lines.ids, + }, + ) + message = { + "message_type": "warning", + "body": "The quantity scanned for one or more lines " + "cannot be higher than the maximum allowed. (%s : %s > %s)" + % (line.product_id.name, line.qty_done, line.reserved_qty), + } + next_picking = first( + self.batch.picking_ids.filtered(lambda p: p.is_shopfloor_packing_todo) + ) + data = self.data.select_package(next_picking, lines) + # data = self.data_detail.pack_picking_detail(next_picking) + self.assert_response( + response, next_state="select_package", data=data, message=message + ) + + def test_pack_package_type(self): + self._create_package_type() self.menu.sudo().write( { "pick_pack_same_time": True, @@ -315,7 +392,7 @@ def test_pack_package_type(self): ) # We use new pack response = self.service.dispatch( - "list_delivery_packaging", + "list_delivery_package_types", params={ "picking_batch_id": self.batch.id, "picking_id": picking.id, @@ -348,3 +425,132 @@ def test_pack_package_type(self): self.assert_response( response, next_state="pack_picking_scan_pack", data=data, message=message ) + + def test_pack_package_type_scan(self): + self._create_package_type() + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + picking.carrier_id = self.carrier + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + + # we scan the package type + response = self.service.dispatch( + "scan_package_action", + params={ + "picking_id": picking.id, + "selected_line_ids": lines.ids, + "barcode": "BOX-5", + }, + ) + message = self.service.msg_store.stock_picking_packed_successfully(picking) + next_picking = first( + self.batch.picking_ids.filtered(lambda p: p.is_shopfloor_packing_todo) + ) + data = self.data_detail.pack_picking_detail(next_picking) + self.assert_response( + response, next_state="pack_picking_scan_pack", message=message, data=data + ) + + def test_pack_package_type_no_picking(self): + self._create_package_type() + self.menu.sudo().write( + { + "pick_pack_same_time": True, + "default_pack_pickings_action": "package_type", + } + ) + + move_lines = self.move_lines + self._set_dest_package_and_done(move_lines[:1], self.bin2) + self._set_dest_package_and_done(move_lines[1:], self.bin1) + move_lines.write({"location_dest_id": self.packing_location.id}) + response = self.service.dispatch( + "prepare_unload", params={"picking_batch_id": self.batch.id} + ) + # The first bin to process is bin1 we should therefore scan the bin 1 + # to pack and put in pack + picking = move_lines[-1].picking_id + picking.carrier_id = self.carrier + data = self.data_detail.pack_picking_detail(picking) + self.assert_response( + response, + next_state="pack_picking_scan_pack", + data=data, + ) + lines = picking.move_line_ids.filtered( + lambda ml: ml.result_package_id.is_internal + ).sorted(key=lambda ml: ml.result_package_id.name) + # we scan the pack + response = self.service.dispatch( + "scan_packing_to_pack", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "barcode": self.bin1.name, + }, + ) + + data = self.data.select_package(picking, lines) + self.assert_response( + response, + next_state="select_package", + data=data, + ) + + # Another action validated the picking + picking._action_done() + + # We use new pack + response = self.service.dispatch( + "list_delivery_package_types", + params={ + "picking_batch_id": self.batch.id, + "picking_id": picking.id, + "selected_line_ids": lines.ids, + }, + ) + data = {} + data["packaging"] = self.data.delivery_packaging_list(self.package_types) + data["picking"] = self.data.picking(picking) + message = {"message_type": "info", "body": "Operation already processed."} + data = {} + self.assert_response(response, next_state="start", data=data, message=message) From a8ae5568c94ba1d514872351f6ee9b0f9ce213b0 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 21 Jan 2025 11:29:16 +0100 Subject: [PATCH 7/9] [IMP] shopfloor_mobile_packing: Update list_delivery_packaging endpoint --- shopfloor_mobile_packing/static/src/js/cluster-picking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js index 109d96b406e..d14f2bb4abe 100644 --- a/shopfloor_mobile_packing/static/src/js/cluster-picking.js +++ b/shopfloor_mobile_packing/static/src/js/cluster-picking.js @@ -263,7 +263,7 @@ let data = function () { */ let endpoint, endpoint_data; const data = this.state.data; - endpoint = "list_delivery_packaging"; + endpoint = "list_delivery_package_types"; endpoint_data = { picking_batch_id: this.current_batch().id, picking_id: data.picking.id, From d4dc7a1b8b121d5f2ec71387ea29d39c944b59fe Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 21 Jan 2025 11:31:11 +0100 Subject: [PATCH 8/9] [IMP] shopfloor_packing: Update docstring for package types --- shopfloor_packing/services/cluster_picking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 5c3cd5978d5..2c6c283c9d4 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -63,7 +63,7 @@ def scan_package_action(self, picking_id, selected_line_ids, barcode) -> dict: When a lot is scanned, it does the same as for the products but based on the lot. - When a packaging type (one without related product) is scanned, a new + When a package type is scanned, a new package is created and set as destination of the lines to pack. Lines to pack are move lines in the list of ``selected_line_ids`` From b688e533816fb570c6b9eb2d12f3994461dfced2 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 21 Jan 2025 11:49:51 +0100 Subject: [PATCH 9/9] [IMP] shopfloor_mobile_packing: Improve docstrings --- shopfloor_mobile_packing/static/src/js/cluster-picking.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopfloor_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js index d14f2bb4abe..e9f96fc12cc 100644 --- a/shopfloor_mobile_packing/static/src/js/cluster-picking.js +++ b/shopfloor_mobile_packing/static/src/js/cluster-picking.js @@ -258,8 +258,8 @@ let data = function () { }, on_new_pack: () => { /** - * Trigger the call to list delivery packaging types - * as user wants to put porducts in a new pack. + * Trigger the call to list delivery package types + * as user wants to put products in a new pack. */ let endpoint, endpoint_data; const data = this.state.data;