diff --git a/shopfloor/actions/message.py b/shopfloor/actions/message.py index e8962bf19bc..5835e401d86 100644 --- a/shopfloor/actions/message.py +++ b/shopfloor/actions/message.py @@ -526,12 +526,6 @@ def packaging_not_found_in_picking(self): "body": _("Packaging not found in the current transfer."), } - def packaging_dimension_updated(self, packaging): - return { - "message_type": "success", - "body": _("Packaging {} dimension updated.").format(packaging.name), - } - def expiration_date_missing(self): return { "message_type": "error", diff --git a/shopfloor_reception_packaging_dimension/__init__.py b/shopfloor_reception_packaging_dimension/__init__.py index e7386302bbe..d354f467795 100644 --- a/shopfloor_reception_packaging_dimension/__init__.py +++ b/shopfloor_reception_packaging_dimension/__init__.py @@ -1,3 +1,4 @@ from .hooks import post_init_hook, uninstall_hook from . import models from . import services +from . import actions diff --git a/shopfloor_reception_packaging_dimension/actions/__init__.py b/shopfloor_reception_packaging_dimension/actions/__init__.py new file mode 100644 index 00000000000..7bb61dcadf7 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/actions/__init__.py @@ -0,0 +1,3 @@ +from . import data +from . import schema +from . import message diff --git a/shopfloor_reception_packaging_dimension/actions/data.py b/shopfloor_reception_packaging_dimension/actions/data.py new file mode 100644 index 00000000000..d79286dae93 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/actions/data.py @@ -0,0 +1,25 @@ +from odoo.addons.component.core import Component +from odoo.addons.shopfloor_base.utils import ensure_model + + +class DataAction(Component): + _inherit = "shopfloor.data.action" + + @property + def _packaging_dimension_detail_parser(self): + return [ + "id", + "name", + "qty", + "packaging_length", + "width", + "height", + "weight", + "length_uom_name", + "weight_uom_name", + "barcode", + ] + + @ensure_model("product.packaging") + def packaging_dimensions(self, record, **kw): + return self._jsonify(record, self._packaging_dimension_detail_parser, **kw) diff --git a/shopfloor_reception_packaging_dimension/actions/message.py b/shopfloor_reception_packaging_dimension/actions/message.py new file mode 100644 index 00000000000..c15e458fda6 --- /dev/null +++ b/shopfloor_reception_packaging_dimension/actions/message.py @@ -0,0 +1,17 @@ +import logging + +from odoo import _ + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class MessageAction(Component): + _inherit = "shopfloor.message.action" + + def packaging_updated(self, packaging): + return { + "message_type": "success", + "body": _("Packaging '{}' updated.").format(packaging.name), + } diff --git a/shopfloor_reception_packaging_dimension/actions/schema.py b/shopfloor_reception_packaging_dimension/actions/schema.py new file mode 100644 index 00000000000..e982635203e --- /dev/null +++ b/shopfloor_reception_packaging_dimension/actions/schema.py @@ -0,0 +1,27 @@ +from odoo.addons.component.core import Component + + +class ShopfloorSchemaAction(Component): + _inherit = "shopfloor.schema.action" + + def packaging_dimensions(self): + return { + "id": {"required": True, "type": "integer"}, + "name": {"type": "string", "nullable": False, "required": True}, + "qty": {"type": "float", "required": True}, + "packaging_length": {"type": "float", "nullable": True, "required": False}, + "width": {"type": "float", "nullable": True, "required": False}, + "height": {"type": "float", "nullable": True, "required": False}, + "weight": {"type": "float", "nullable": True, "required": False}, + "length_uom_name": { + "type": "string", + "nullable": True, + "required": False, + }, + "weight_uom_name": { + "type": "string", + "nullable": True, + "required": False, + }, + "barcode": {"type": "string", "nullable": True, "required": False}, + } diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index bcce750e136..f44a671ddce 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -17,13 +17,16 @@ def __init__(self, work_context): def _before_state__set_quantity(self, picking, line, message=None): """Show the packaging dimension screen before the set quantity screen.""" - if self.work.menu.set_packaging_dimension and not self.packaging_update_done: - packaging = self._get_next_packaging_to_set_dimension(line.product_id) - if packaging: - return self._response_for_set_packaging_dimension( - picking, line, packaging, message=message - ) - return super()._before_state__set_quantity(picking, line, message=message) + if not self.work.menu.set_packaging_dimension or self.packaging_update_done: + return super()._before_state__set_quantity(picking, line, message=message) + + packaging = self._get_next_packaging_to_set_dimension(line.product_id) + if not packaging: + return super()._before_state__set_quantity(picking, line, message=message) + + return self._response_for_set_packaging_dimension( + picking, line, packaging, message=message + ) def _get_domain_packaging_needs_dimension(self): return expression.OR( @@ -86,14 +89,14 @@ def _response_for_set_packaging_dimension( ) def _set_packaging_dimension_data_for_packaging(self, packaging): - return self.data_detail.packaging_detail(packaging) + return self.data.packaging_dimensions(packaging) def set_packaging_dimension( - self, picking_id, selected_line_id, packaging_id, cancel=False, **kwargs + self, picking_id, selected_line_id, packaging_id, skip=False, **kwargs ): """Set the dimension on a product packaging. - If the user cancel the dimension update we still propose the next + If the user skip the dimension update we still propose the next possible packaging. Transitions: @@ -103,38 +106,48 @@ def set_packaging_dimension( picking = self.env["stock.picking"].browse(picking_id) selected_line = self.env["stock.move.line"].browse(selected_line_id) packaging = self.env["product.packaging"].sudo().browse(packaging_id) - message = None - next_packaging = None + if not packaging: - message = self.msg_store.record_not_found() - elif not cancel and self._check_dimension_to_update(kwargs): - self._update_packaging_dimension(packaging, kwargs) - message = self.msg_store.packaging_dimension_updated(packaging) - if packaging: - next_packaging = self._get_next_packaging_to_set_dimension( - selected_line.product_id, packaging + return self._before_state__set_quantity( + picking, selected_line, message=self.msg_store.record_not_found() ) + + message = None + + if not skip and self._check_dimension_to_update(kwargs): + self._update_packaging_dimension(packaging, kwargs) + message = self.msg_store.packaging_updated(packaging) + + next_packaging = self._get_next_packaging_to_set_dimension( + selected_line.product_id, packaging + ) if next_packaging: return self._response_for_set_packaging_dimension( picking, selected_line, next_packaging, message=message ) + self.packaging_update_done = True return self._before_state__set_quantity(picking, selected_line, message=message) def _check_dimension_to_update(self, dimensions): - """Return True if any dimension on the packaging needs to be updated""" - return any([value is not None for key, value in dimensions.items()]) - - def _get_dimension_fields_conversion_map(self): - return {"length": "packaging_length"} + """Check if the Shopfloor payload contains data for a packaging update.""" + return any(value is not None for value in dimensions.values()) def _update_packaging_dimension(self, packaging, dimensions_to_update): """Update dimension on the packaging.""" - fields_conv_map = self._get_dimension_fields_conversion_map() - for dimension, value in dimensions_to_update.items(): - if value is not None: - dimension = fields_conv_map.get(dimension, dimension) - packaging[dimension] = value + values_to_update = {} + packaging_values = packaging.read(dimensions_to_update.keys())[0] + + for key, value in dimensions_to_update.items(): + if value is None: + continue + # Skip updating fields with unchanged values to prevent unnecessary + # triggers of compute methods or other side effects + if packaging_values[key] != value: + values_to_update[key] = value + + if values_to_update: + packaging.write(values_to_update) class ShopfloorReceptionValidator(Component): @@ -155,7 +168,7 @@ def set_packaging_dimension(self): "type": "float", "nullable": True, }, - "length": { + "packaging_length": { "coerce": to_float, "required": False, "type": "float", @@ -186,7 +199,7 @@ def set_packaging_dimension(self): "nullable": True, }, "barcode": {"type": "string", "required": False, "nullable": True}, - "cancel": {"type": "boolean"}, + "skip": {"type": "boolean"}, } @@ -213,13 +226,13 @@ def _schema_set_packaging_dimension(self): return { "picking": {"type": "dict", "schema": self.schemas.picking()}, "selected_move_line": {"type": "dict", "schema": self.schemas.move_line()}, - "packaging": self._schema_packaging(), + "packaging": self._schema_packaging_dimensions(), } - def _schema_packaging(self): + def _schema_packaging_dimensions(self): return { "type": "dict", - "schema": self.schemas_detail.packaging_detail(), + "schema": self.schemas.packaging_dimensions(), } def _set_packaging_dimension_next_states(self): diff --git a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py index d04d9e335c7..41360e23221 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -53,7 +53,7 @@ def _assert_response_set_dimension( data = { "picking": self.data.picking(picking), "selected_move_line": self.data.move_line(line), - "packaging": self.data_detail.packaging_detail(packaging), + "packaging": self.data.packaging_dimensions(packaging), } self.assert_response( response, @@ -206,7 +206,7 @@ def test_set_multiple_packaging_dimension(self): "selected_line_id": line.id, "packaging_id": self.product_c_packaging.id, "height": 55, - "length": 233, + "packaging_length": 233, }, ) self.assertEqual(self.product_c_packaging.height, 55) @@ -216,9 +216,7 @@ def test_set_multiple_packaging_dimension(self): self.picking, line, self.product_c_packaging_2, - message=self.msg_store.packaging_dimension_updated( - self.product_c_packaging - ), + message=self.msg_store.packaging_updated(self.product_c_packaging), ) response = self.service.dispatch( "set_packaging_dimension", @@ -240,7 +238,35 @@ def test_set_multiple_packaging_dimension(self): "selected_move_line": self.data.move_lines(line), "confirmation_required": None, }, - message=self.msg_store.packaging_dimension_updated( - self.product_c_packaging_2 - ), + message=self.msg_store.packaging_updated(self.product_c_packaging_2), + ) + + def test_skip_packaging_dimension_skips_to_next(self): + line = self.picking.move_line_ids.filtered( + lambda li: li.product_id == self.product_c + ) + original_height = self.product_c_packaging.height + + response = self.service.dispatch( + "set_packaging_dimension", + params={ + "picking_id": self.picking.id, + "selected_line_id": line.id, + "packaging_id": self.product_c_packaging.id, + "height": 999.0, # This value should be ignored + "skip": True, + }, + ) + + self.assertEqual( + self.product_c_packaging.height, + original_height, + "Packaging height should not change when skipped", + ) + self._assert_response_set_dimension( + response, + self.picking, + line, + self.product_c_packaging_2, + message=None, # No 'Updated' message should be returned when skipping ) diff --git a/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js index 30138f958c8..bb143fa79e6 100644 --- a/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js +++ b/shopfloor_reception_packaging_dimension_mobile/static/src/scenario/reception_packaging_dimension.js @@ -6,7 +6,6 @@ import {process_registry} from "/shopfloor_mobile_base/static/wms/src/services/process_registry.js"; const reception_scenario = process_registry.get("reception"); -const _get_states = reception_scenario.component.methods._get_states; // Get the original template of the reception scenario const template = reception_scenario.component.template; // And inject the new state template (for this module) into it @@ -19,7 +18,7 @@ const new_template = @@ -28,7 +27,7 @@ const new_template = @@ -36,46 +35,45 @@ const new_template = label="Quantiy" type="number" placeholder="Packaging Quantity" - v-model="state.data.packaging.qty" + v-model="state.data.packaging.qty_input" > - @@ -101,42 +99,159 @@ const new_template = // Extend the reception scenario with : // - the new patched template // - the js code for the new state +const _get_states_base = reception_scenario.component.methods._get_states; +const baseWatchers = reception_scenario.component.watch || {}; +const baseMethods = reception_scenario.component.methods || {}; const ReceptionPackageDimension = process_registry.extend("reception", { template: new_template, - "methods.get_packaging_measurements": function () { - return ["length", "width", "height", "weight", "qty", "barcode"]; + watch: { + ...baseWatchers, + "state.key": function (newState) { + if (newState === "set_packaging_dimension") { + this.prefill_packaging_form_inputs(); + } + }, }, - "methods._get_states": function () { - let states = _get_states.bind(this)(); - states["set_packaging_dimension"] = { - display_info: { - title: "Set packaging dimension", - }, - events: { - go_back: "on_back", - }, - get_payload_set_packaging_dimension: () => { - let values = { - picking_id: this.state.data.picking.id, - selected_line_id: this.state.data.selected_move_line.id, - packaging_id: this.state.data.packaging.id, - }; - for (const measurement of this.get_packaging_measurements()) { - values[measurement] = this.state.data.packaging[measurement]; + methods: { + ...baseMethods, + prefill_packaging_form_inputs: function () { + if (!this.state_is("set_packaging_dimension")) return; + + const pkg = this.state.data.packaging; + const input_fields = this.get_packaging_measurements_inputs(); + + input_fields.forEach((inputKey) => { + const originalKey = inputKey.replace("_input", ""); + if (pkg[inputKey] === undefined || pkg[inputKey] === null) { + this.$set(pkg, inputKey, pkg[originalKey]); } - return values; - }, - on_skip: () => { - const payload = this.state.get_payload_set_packaging_dimension(); - payload["cancel"] = true; - this.wait_call(this.odoo.call("set_packaging_dimension", payload)); - }, - on_done: () => { - const payload = this.state.get_payload_set_packaging_dimension(); - this.wait_call(this.odoo.call("set_packaging_dimension", payload)); - }, - }; - return states; + }); + }, + get_packaging_measurements_inputs: function () { + return [ + "packaging_length_input", + "width_input", + "height_input", + "weight_input", + "qty_input", + "barcode_input", + ]; + }, + packaging_detail_options: function () { + const pkg = this.state.data.packaging; + const _is_field_changed = (fieldName) => { + const inputKey = fieldName + "_input"; + return pkg[inputKey] && pkg[inputKey] != pkg[fieldName]; + }; + const options = { + main: true, + key_title: "name", + title_icon: "mdi-package-variant", + fields: [ + { + path: "barcode", + label: "Barcode", + klass: _is_field_changed("barcode") ? "accent" : "", + }, + { + path: "qty", + label: "Quantity", + klass: _is_field_changed("qty") ? "accent" : "", + }, + { + path: "packaging_length", + label: "Length", + klass: _is_field_changed("packaging_length") ? "accent" : "", + renderer: function (rec, field) { + const value = _.result(rec, "packaging_length", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "width", + label: "Width", + klass: _is_field_changed("width") ? "accent" : "", + renderer: function (rec, field) { + const value = _.result(rec, "width", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "height", + label: "Height", + klass: _is_field_changed("height") ? "accent" : "", + renderer: function (rec, field) { + const value = _.result(rec, "height", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "weight", + label: "Weight", + klass: _is_field_changed("weight") ? "accent" : "", + renderer: function (rec, field) { + const value = _.result(rec, "weight", ""); + const uom = _.result(rec, "weight_uom_name", ""); + return value + " " + uom; + }, + }, + ], + }; + return options; + }, + _get_states: function () { + let states = _get_states_base.bind(this)(); + + // Capture 'this' in a variable to be safe across async boundaries + const self = this; + + states["set_packaging_dimension"] = { + display_info: { + title: "Set packaging dimension", + }, + events: { + go_back: "on_back", + }, + _get_payload_set_packaging_dimension: () => { + let values = { + picking_id: this.state.data.picking.id, + selected_line_id: this.state.data.selected_move_line.id, + packaging_id: this.state.data.packaging.id, + }; + for (const measurement of this.get_packaging_measurements_inputs()) { + values[measurement.replace("_input", "")] = + this.state.data.packaging[measurement]; + } + return values; + }, + _handle_dimension_submission: async function (is_skip = false) { + const payload = self.state._get_payload_set_packaging_dimension(); + if (is_skip) { + payload["skip"] = true; + } + + await self.wait_call( + self.odoo.call("set_packaging_dimension", payload) + ); + + // Prepare next screen + self.prefill_packaging_form_inputs(); + self.$nextTick(() => { + window.scrollTo(0, 0); + }); + }, + on_skip: async function () { + await self.state._handle_dimension_submission(true); + }, + on_done: async function () { + await self.state._handle_dimension_submission(false); + }, + }; + return states; + }, }, });