diff --git a/shopfloor_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js index cb9ed73a11..109d96b406 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 96342a8a66..32b1c8f1f4 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/data.py b/shopfloor_packing/actions/data.py index e8d036e7c9..dc36ffabb7 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/actions/schema.py b/shopfloor_packing/actions/schema.py index a59a168516..74cc9642bc 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 7786b41946..630dd15bcf 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 b2faa17efc..ac4596b132 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 61fd381dbb..0909fb2ba9 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -6,10 +6,118 @@ 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) + + @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 +218,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 +233,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 +251,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 +293,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 +305,97 @@ 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 _response_for_select_package(self, picking, lines, message=None): + 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): + 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( + line + ), + ) + class ShopfloorClusterPickingValidator(Component): """Validators for the Cluster Picking endpoints.""" @@ -208,7 +410,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 +425,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 +470,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 +489,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 +512,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 0000000000..1094051575 --- /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 e84b11d98c..b3db59427f 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_pack_picking, + test_cluster_picking_pick_pack, + test_cluster_picking_unload, +) diff --git a/shopfloor_packing/tests/common.py b/shopfloor_packing/tests/common.py new file mode 100644 index 0000000000..0655d67022 --- /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 22a44f761b..1be91709e0 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 0000000000..0eb3ee893f --- /dev/null +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -0,0 +1,111 @@ +# Copyright 2024 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, + ) + 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 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 + ) + 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.bin2.name, + }, + ) + data = self.data.select_package(picking, lines) + 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": 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 d24718b4c0..d251ba72d1 100644 --- a/shopfloor_packing/views/shopfloor_menu.xml +++ b/shopfloor_packing/views/shopfloor_menu.xml @@ -14,6 +14,7 @@ attrs="{'invisible': [('scenario', '!=', 'cluster_picking')]}" > + diff --git a/test-requirements.txt b/test-requirements.txt index 0d19f48858..5c53a5e2f5 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