diff --git a/shopfloor_mobile_packing/static/src/js/cluster-picking.js b/shopfloor_mobile_packing/static/src/js/cluster-picking.js index cb9ed73a115..e9f96fc12cc 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 package types + * as user wants to put products in a new pack. + */ + let endpoint, endpoint_data; + const data = this.state.data; + endpoint = "list_delivery_package_types"; + 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/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/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..265d3f60038 100644 --- a/shopfloor_packing/services/__init__.py +++ b/shopfloor_packing/services/__init__.py @@ -1 +1 @@ -from . import cluster_picking +from . import cluster_picking, packing diff --git a/shopfloor_packing/services/cluster_picking.py b/shopfloor_packing/services/cluster_picking.py index 61fd381dbbd..2c6c283c9d4 100644 --- a/shopfloor_packing/services/cluster_picking.py +++ b/shopfloor_packing/services/cluster_picking.py @@ -1,57 +1,115 @@ # 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 class ClusterPicking(Component): + _inherit = "shopfloor.cluster.picking" - 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) - ) + # PUBLIC METHODS - ENDPOINTS - def _get_next_picking_to_pack(self, batch): - """ - Return a picking not yet packed. + def list_delivery_package_types( + self, picking_batch_id, picking_id, selected_line_ids + ) -> dict: + """List available delivery package types for given picking. - The returned picking is the first - one into the list of picking not yet packed (is_shopfloor_packing_todo=True). - nbr_packages + Transitions: + * 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 package types are available """ - pickings_to_pack = batch.picking_ids.filtered( - lambda p: p.is_shopfloor_packing_todo + 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_package_type(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_package_type( + picking, delivery_packaging ) - 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 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 - 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 + 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 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`` + 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 + """ + 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 = 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, + 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( + picking.batch_id.id, picking.id, package_type_id=package_type_id ) - 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) @@ -71,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() @@ -110,18 +168,7 @@ def scan_packing_to_pack(self, picking_batch_id, picking_id, barcode): batch, ) - def _prepare_pack_picking(self, batch, message=None): - picking = self._get_next_picking_to_pack(batch) - 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) - 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) @@ -131,30 +178,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 + ) -> dict: 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 + packing_action: PackingAction = self._actions_for("packing") + result = packing_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() @@ -168,12 +215,105 @@ def put_in_pack(self, picking_batch_id, picking_id, nbr_packages): 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): + 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 ) @@ -185,10 +325,108 @@ 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): + 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_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) -> dict: + 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 _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_package_type(packaging), + }, + message=message, + ) + + 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 @@ -200,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, @@ -208,10 +446,22 @@ 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) -> dict: + return { + "picking_batch_id": { + "coerce": to_int, + "required": True, + "type": "integer", + }, + "picking_id": {"coerce": to_int, "required": True, "type": "integer"}, + "barcode": {"required": True, "type": "string"}, } - def scan_packing_to_pack(self): + def list_delivery_package_types(self) -> dict: return { "picking_batch_id": { "coerce": to_int, @@ -219,6 +469,21 @@ def scan_packing_to_pack(self): "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) -> dict: + 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"}, } @@ -228,44 +493,78 @@ class ShopfloorClusterPickingValidatorResponse(Component): _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 - 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} - def prepare_unload(self): + @property + def _schema_select_package(self) -> dict: + schema = self.schemas.select_package() + return {"type": "dict", "nullable": True, "schema": schema} + + 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 scan_destination_pack(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) -> 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", "unload_single", "pack_picking_put_in_pack", "pack_picking_scan_pack", + "select_package", } ) + + 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) -> dict: + return { + "picking": {"type": "dict", "schema": self.schemas.picking()}, + "packaging": self.schemas._schema_list_of( + self.schemas.delivery_packaging() + ), + } + + def scan_package_action(self) -> dict: + return self.prepare_unload() diff --git a/shopfloor_packing/services/packing.py b/shopfloor_packing/services/packing.py new file mode 100644 index 00000000000..66748872d29 --- /dev/null +++ b/shopfloor_packing/services/packing.py @@ -0,0 +1,78 @@ +# 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 PackingAction(Component): + + _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, + 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/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/__init__.py b/shopfloor_packing/tests/__init__.py index e84b11d98c7..b3db59427f7 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 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..ad256cc31ea --- /dev/null +++ b/shopfloor_packing/tests/test_cluster_picking_pick_pack.py @@ -0,0 +1,556 @@ +# 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) + 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) + + 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_package_types", + 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_package_types", + 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_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", + } + ) + + 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, + ) + 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, + "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_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) + 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 + ) + + 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) 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')]}" > + 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