From bc5b7123f2f3837a479cc814bbaa4f6bb170b952 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 19 Feb 2026 12:02:46 +0100 Subject: [PATCH 01/12] [IMP] shopfloor_reception_packaging_dimension: use custom packaging parser By introducing a specific packaging parser, we ensure we get exactly the infos we need for the "packaging dimension" scenario --- .../__init__.py | 1 + .../actions/__init__.py | 2 ++ .../actions/data.py | 25 +++++++++++++++++ .../actions/schema.py | 27 +++++++++++++++++++ .../services/reception.py | 8 +++--- .../tests/test_set_package_dimension.py | 2 +- 6 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 shopfloor_reception_packaging_dimension/actions/__init__.py create mode 100644 shopfloor_reception_packaging_dimension/actions/data.py create mode 100644 shopfloor_reception_packaging_dimension/actions/schema.py 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..0049231d56c --- /dev/null +++ b/shopfloor_reception_packaging_dimension/actions/__init__.py @@ -0,0 +1,2 @@ +from . import data +from . import schema diff --git a/shopfloor_reception_packaging_dimension/actions/data.py b/shopfloor_reception_packaging_dimension/actions/data.py new file mode 100644 index 00000000000..b752e41c52b --- /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: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/schema.py b/shopfloor_reception_packaging_dimension/actions/schema.py new file mode 100644 index 00000000000..6d17c19ab73 --- /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}, + "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..f10c55cfc75 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -86,7 +86,7 @@ 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 @@ -213,13 +213,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..fa4b2709585 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, From fb94bef5aaf7b4487e71bb61c73ae6ce09369c2f Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 19 Feb 2026 13:45:41 +0100 Subject: [PATCH 02/12] [IMP] shopfloor_reception_packaging_dimension_mobile: show selected package details --- .../scenario/reception_packaging_dimension.js | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) 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..922920dd090 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 @@ -19,7 +19,7 @@ const new_template = @@ -43,7 +43,7 @@ const new_template = @@ -52,7 +52,7 @@ const new_template = @@ -61,7 +61,7 @@ const new_template = @@ -70,7 +70,7 @@ const new_template = @@ -106,6 +106,54 @@ const ReceptionPackageDimension = process_registry.extend("reception", { "methods.get_packaging_measurements": function () { return ["length", "width", "height", "weight", "qty", "barcode"]; }, + "methods.packaging_detail_options": function () { + const options = { + main: true, + key_title: "name", + title_icon: "mdi-package-variant", + fields: [ + {path: "barcode", label: "Barcode"}, + {path: "qty", label: "Quantity"}, + { + path: "length", + label: "Length", + renderer: function (rec, field) { + const value = _.result(rec, "length", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "width", + label: "Width", + renderer: function (rec, field) { + const value = _.result(rec, "width", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "height", + label: "Height", + renderer: function (rec, field) { + const value = _.result(rec, "height", ""); + const uom = _.result(rec, "length_uom_name", ""); + return value + " " + uom; + }, + }, + { + path: "weight", + label: "Weight", + renderer: function (rec, field) { + const value = _.result(rec, "weight", ""); + const uom = _.result(rec, "weight_uom_name", ""); + return value + " " + uom; + }, + }, + ], + }; + return options; + }, "methods._get_states": function () { let states = _get_states.bind(this)(); states["set_packaging_dimension"] = { From 0b78b7cedb1010bcef1d9cbcceb5896bedac122e Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Thu, 19 Feb 2026 14:42:04 +0100 Subject: [PATCH 03/12] [REF] shopfloor_reception_packaging_dimension: localize and refine update message - Move the 'packaging_dimension_updated' message from shopfloor to this module, as it is only relevant here. - Rename/Update the message to 'packaging_updated' and remove the word "dimension", as the feature also allows updating barcodes and quantity. --- shopfloor/actions/message.py | 6 ------ .../actions/__init__.py | 1 + .../actions/message.py | 17 +++++++++++++++++ .../services/reception.py | 2 +- .../tests/test_set_package_dimension.py | 8 ++------ 5 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 shopfloor_reception_packaging_dimension/actions/message.py 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/actions/__init__.py b/shopfloor_reception_packaging_dimension/actions/__init__.py index 0049231d56c..7bb61dcadf7 100644 --- a/shopfloor_reception_packaging_dimension/actions/__init__.py +++ b/shopfloor_reception_packaging_dimension/actions/__init__.py @@ -1,2 +1,3 @@ from . import data from . import schema +from . import message 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/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index f10c55cfc75..53344339030 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -109,7 +109,7 @@ def set_packaging_dimension( 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) + message = self.msg_store.packaging_updated(packaging) if packaging: next_packaging = self._get_next_packaging_to_set_dimension( selected_line.product_id, packaging 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 fa4b2709585..d736046e22d 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -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,5 @@ 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), ) From a171d9d8911c7b121c804b3a38b8ba9adc98be46 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Fri, 20 Feb 2026 16:18:46 +0100 Subject: [PATCH 04/12] [REF] shopfloor_reception_packaging_dimension: general code improvements - Use guard clause pattern - Update doctstring - Replace list comprehension with a generator expression to avoid allocating useless list in memory --- .../services/reception.py | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index 53344339030..277f7a92b57 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( @@ -103,38 +106,54 @@ 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): + return self._before_state__set_quantity( + picking, selected_line, message=self.msg_store.record_not_found() + ) + + message = None + + if not cancel and self._check_dimension_to_update(kwargs): self._update_packaging_dimension(packaging, kwargs) message = self.msg_store.packaging_updated(packaging) - if packaging: - next_packaging = self._get_next_packaging_to_set_dimension( - selected_line.product_id, 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()]) + """Check if the Shopfloor payload contains data for a packaging update.""" + return any(value is not None for value in dimensions.values()) def _get_dimension_fields_conversion_map(self): + """ + Get the mapping between JSON keys from the Shopfloor interface + and the technical field names of the product.packaging model. + """ return {"length": "packaging_length"} 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 + field_map = self._get_dimension_fields_conversion_map() + values_to_update = {} + + for key, value in dimensions_to_update.items(): + if value is None: + continue + + odoo_field = field_map.get(key, key) + values_to_update[odoo_field] = value + + if values_to_update: + packaging.write(values_to_update) class ShopfloorReceptionValidator(Component): From 2dc41c92585a72979ac72b3885475674298f361e Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Fri, 20 Feb 2026 17:00:40 +0100 Subject: [PATCH 05/12] [REF] shopfloor_reception_packaging_dimension: rename 'cancel' to 'skip' Rename the 'cancel' parameter to 'skip' in the set_packaging_dimension service and validator. The term 'skip' is more accurate because the process continues to the next packaging or step rather than aborting some operation. This also aligns the backend logic with the 'Skip' button label used in the Shopfloor UI, improving developer clarity. --- .../services/reception.py | 8 ++++---- .../tests/test_set_package_dimension.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index 277f7a92b57..6599840b731 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -92,11 +92,11 @@ def _set_packaging_dimension_data_for_packaging(self, 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: @@ -114,7 +114,7 @@ def set_packaging_dimension( message = None - if not cancel and self._check_dimension_to_update(kwargs): + if not skip and self._check_dimension_to_update(kwargs): self._update_packaging_dimension(packaging, kwargs) message = self.msg_store.packaging_updated(packaging) @@ -205,7 +205,7 @@ def set_packaging_dimension(self): "nullable": True, }, "barcode": {"type": "string", "required": False, "nullable": True}, - "cancel": {"type": "boolean"}, + "skip": {"type": "boolean"}, } 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 d736046e22d..edf63517776 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -240,3 +240,6 @@ def test_set_multiple_packaging_dimension(self): }, message=self.msg_store.packaging_updated(self.product_c_packaging_2), ) + + # TODO: Test that skipping dimension entry (skip=True) for one packaging + # correctly transitions to the next pending packaging without saving changes. From 91fd7b7112e039c9779acffb59f962ae085057f7 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Fri, 20 Feb 2026 17:33:02 +0100 Subject: [PATCH 06/12] [IMP] shopfloor_reception_packaging_dimension_mobile: use input buffers and highlight changes Update the packaging dimension screen to improve data handling and user feedback: - Switch v-model bindings from direct record fields (e.g., 'barcode') to input buffers (e.g., 'barcode_input'). - Implement '_is_field_changed' helper to detect differences between buffered inputs and original record values. - Apply the 'accent' CSS class to detail fields when they have been modified, providing clear visual feedback to the user. - Rename 'get_packaging_measurements' to 'get_packaging_measurements_inputs' to reflect the shift to buffered data. - Align the 'on_skip' payload with the backend rename of 'cancel' to 'skip'. --- .../scenario/reception_packaging_dimension.js | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) 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 922920dd090..5b0be4f6745 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 @@ -28,7 +28,7 @@ const new_template = @@ -36,7 +36,7 @@ const new_template = label="Quantiy" type="number" placeholder="Packaging Quantity" - v-model="state.data.packaging.qty" + v-model="state.data.packaging.qty_input" > @@ -45,7 +45,7 @@ const new_template = type="number" :suffix="state.data.packaging.length_uom_name" placeholder="Packaging Length" - v-model="state.data.packaging.length" + v-model="state.data.packaging.length_input" > @@ -54,7 +54,7 @@ const new_template = type="number" :suffix="state.data.packaging.length_uom_name" placeholder="Packaging Width" - v-model="state.data.packaging.width" + v-model="state.data.packaging.width_input" > @@ -63,7 +63,7 @@ const new_template = type="number" :suffix="state.data.packaging.length_uom_name" placeholder="Packaging Height" - v-model="state.data.packaging.height" + v-model="state.data.packaging.height_input" > @@ -72,7 +72,7 @@ const new_template = type="number" :suffix="state.data.packaging.weight_uom_name" placeholder="Packaging Weight" - v-model="state.data.packaging.weight" + v-model="state.data.packaging.weight_input" > @@ -103,20 +103,41 @@ const new_template = // - the js code for the new state const ReceptionPackageDimension = process_registry.extend("reception", { template: new_template, - "methods.get_packaging_measurements": function () { - return ["length", "width", "height", "weight", "qty", "barcode"]; + "methods.get_packaging_measurements_inputs": function () { + return [ + "length_input", + "width_input", + "height_input", + "weight_input", + "qty_input", + "barcode_input", + ]; }, "methods.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"}, - {path: "qty", label: "Quantity"}, + { + path: "barcode", + label: "Barcode", + klass: _is_field_changed("barcode") ? "accent" : "", + }, + { + path: "qty", + label: "Quantity", + klass: _is_field_changed("qty") ? "accent" : "", + }, { path: "length", label: "Length", + klass: _is_field_changed("length") ? "accent" : "", renderer: function (rec, field) { const value = _.result(rec, "length", ""); const uom = _.result(rec, "length_uom_name", ""); @@ -126,6 +147,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { { 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", ""); @@ -135,6 +157,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { { 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", ""); @@ -144,6 +167,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { { 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", ""); @@ -169,14 +193,15 @@ const ReceptionPackageDimension = process_registry.extend("reception", { 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]; + for (const measurement of this.get_packaging_measurements_inputs()) { + values[measurement.replace("_input", "")] = + this.state.data.packaging[measurement]; } return values; }, on_skip: () => { const payload = this.state.get_payload_set_packaging_dimension(); - payload["cancel"] = true; + payload["skip"] = true; this.wait_call(this.odoo.call("set_packaging_dimension", payload)); }, on_done: () => { From 59fd360fa5c308ec6366710f568e3fa44a1f5620 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 23 Feb 2026 08:30:47 +0100 Subject: [PATCH 07/12] [REF] shopfloor_reception_packaging_dimension/mobile: align UI keys with model fields Simplify the data flow between the Shopfloor UI and the backend by removing the unnecessary field mapping. --- .../actions/data.py | 2 +- .../actions/schema.py | 2 +- .../services/reception.py | 14 ++------------ .../tests/test_set_package_dimension.py | 2 +- .../src/scenario/reception_packaging_dimension.js | 10 +++++----- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/shopfloor_reception_packaging_dimension/actions/data.py b/shopfloor_reception_packaging_dimension/actions/data.py index b752e41c52b..d79286dae93 100644 --- a/shopfloor_reception_packaging_dimension/actions/data.py +++ b/shopfloor_reception_packaging_dimension/actions/data.py @@ -11,7 +11,7 @@ def _packaging_dimension_detail_parser(self): "id", "name", "qty", - "packaging_length:length", + "packaging_length", "width", "height", "weight", diff --git a/shopfloor_reception_packaging_dimension/actions/schema.py b/shopfloor_reception_packaging_dimension/actions/schema.py index 6d17c19ab73..e982635203e 100644 --- a/shopfloor_reception_packaging_dimension/actions/schema.py +++ b/shopfloor_reception_packaging_dimension/actions/schema.py @@ -9,7 +9,7 @@ def packaging_dimensions(self): "id": {"required": True, "type": "integer"}, "name": {"type": "string", "nullable": False, "required": True}, "qty": {"type": "float", "required": True}, - "length": {"type": "float", "nullable": True, "required": False}, + "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}, diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index 6599840b731..c68e609d5ec 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -133,24 +133,14 @@ def _check_dimension_to_update(self, dimensions): """Check if the Shopfloor payload contains data for a packaging update.""" return any(value is not None for value in dimensions.values()) - def _get_dimension_fields_conversion_map(self): - """ - Get the mapping between JSON keys from the Shopfloor interface - and the technical field names of the product.packaging model. - """ - return {"length": "packaging_length"} - def _update_packaging_dimension(self, packaging, dimensions_to_update): """Update dimension on the packaging.""" - field_map = self._get_dimension_fields_conversion_map() values_to_update = {} for key, value in dimensions_to_update.items(): if value is None: continue - - odoo_field = field_map.get(key, key) - values_to_update[odoo_field] = value + values_to_update[key] = value if values_to_update: packaging.write(values_to_update) @@ -174,7 +164,7 @@ def set_packaging_dimension(self): "type": "float", "nullable": True, }, - "length": { + "packaging_length": { "coerce": to_float, "required": False, "type": "float", 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 edf63517776..ffa4edf6eea 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -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) 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 5b0be4f6745..41638f8bac2 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 @@ -45,7 +45,7 @@ const new_template = type="number" :suffix="state.data.packaging.length_uom_name" placeholder="Packaging Length" - v-model="state.data.packaging.length_input" + v-model="state.data.packaging.packaging_length_input" > @@ -105,7 +105,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { template: new_template, "methods.get_packaging_measurements_inputs": function () { return [ - "length_input", + "packaging_length_input", "width_input", "height_input", "weight_input", @@ -135,11 +135,11 @@ const ReceptionPackageDimension = process_registry.extend("reception", { klass: _is_field_changed("qty") ? "accent" : "", }, { - path: "length", + path: "packaging_length", label: "Length", - klass: _is_field_changed("length") ? "accent" : "", + klass: _is_field_changed("packaging_length") ? "accent" : "", renderer: function (rec, field) { - const value = _.result(rec, "length", ""); + const value = _.result(rec, "packaging_length", ""); const uom = _.result(rec, "length_uom_name", ""); return value + " " + uom; }, From d0eab78aaba0cc43c245c3d773354b5f464802f9 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 23 Feb 2026 08:36:30 +0100 Subject: [PATCH 08/12] [REF] shopfloor_reception_packaging_dimension: avoid redundant writes Optimize the packaging dimension update logic to skip writing values that are already present in the database. --- .../services/reception.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shopfloor_reception_packaging_dimension/services/reception.py b/shopfloor_reception_packaging_dimension/services/reception.py index c68e609d5ec..f44a671ddce 100644 --- a/shopfloor_reception_packaging_dimension/services/reception.py +++ b/shopfloor_reception_packaging_dimension/services/reception.py @@ -136,11 +136,15 @@ def _check_dimension_to_update(self, dimensions): def _update_packaging_dimension(self, packaging, dimensions_to_update): """Update dimension on the packaging.""" 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 - values_to_update[key] = value + # 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) From 5e9b8e5baf3e26af0361905727a4a8f47992131c Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 23 Feb 2026 08:49:39 +0100 Subject: [PATCH 09/12] [IMP] shopfloor_reception_packaging_dimension: add test case for skipping dimensions --- .../tests/test_set_package_dimension.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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 ffa4edf6eea..41360e23221 100644 --- a/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py +++ b/shopfloor_reception_packaging_dimension/tests/test_set_package_dimension.py @@ -241,5 +241,32 @@ def test_set_multiple_packaging_dimension(self): message=self.msg_store.packaging_updated(self.product_c_packaging_2), ) - # TODO: Test that skipping dimension entry (skip=True) for one packaging - # correctly transitions to the next pending packaging without saving changes. + 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 + ) From 65e9ce5fdfaaba81c5167aa49e07630e3dd2878b Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Mon, 30 Mar 2026 12:24:55 +0200 Subject: [PATCH 10/12] [IMP] shopfloor_reception_packaging_dimension_mobile: pre-fill form input with db values --- .../scenario/reception_packaging_dimension.js | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) 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 41638f8bac2..845b7a0e767 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 @@ -101,8 +101,30 @@ const new_template = // Extend the reception scenario with : // - the new patched template // - the js code for the new state +const baseWatchers = reception_scenario.component.watch || {}; const ReceptionPackageDimension = process_registry.extend("reception", { template: new_template, + watch: { + ...baseWatchers, + "state.key": function (newState) { + if (newState === "set_packaging_dimension") { + this.prefill_packaging_form_inputs(); + } + }, + }, + "methods.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]); + } + }); + }, "methods.get_packaging_measurements_inputs": function () { return [ "packaging_length_input", @@ -180,6 +202,10 @@ const ReceptionPackageDimension = process_registry.extend("reception", { }, "methods._get_states": function () { let states = _get_states.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", @@ -199,14 +225,20 @@ const ReceptionPackageDimension = process_registry.extend("reception", { } return values; }, - on_skip: () => { - const payload = this.state.get_payload_set_packaging_dimension(); + on_skip: async function () { + const payload = self.state.get_payload_set_packaging_dimension(); payload["skip"] = true; - this.wait_call(this.odoo.call("set_packaging_dimension", payload)); + await self.wait_call( + self.odoo.call("set_packaging_dimension", payload) + ); + self.prefill_packaging_form_inputs(); }, - on_done: () => { - const payload = this.state.get_payload_set_packaging_dimension(); - this.wait_call(this.odoo.call("set_packaging_dimension", payload)); + on_done: async function () { + const payload = self.state.get_payload_set_packaging_dimension(); + await self.wait_call( + self.odoo.call("set_packaging_dimension", payload) + ); + self.prefill_packaging_form_inputs(); }, }; return states; From f0bc46c3963e1e2f2e1186c262665bed2b530569 Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Wed, 8 Apr 2026 11:04:24 +0200 Subject: [PATCH 11/12] [REF] shopfloor_reception_packaging_dimension_mobile: refactor methods definition The previous implementation used flattened string keys (e.g., "methods.name") to define scenario methods. While functional, this is less idiomatic and prevents editor support --- .../scenario/reception_packaging_dimension.js | 251 +++++++++--------- 1 file changed, 127 insertions(+), 124 deletions(-) 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 845b7a0e767..04edb4bc006 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 @@ -75,7 +74,6 @@ const new_template = v-model="state.data.packaging.weight_input" > - @@ -101,7 +99,9 @@ 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, watch: { @@ -112,136 +112,139 @@ const ReceptionPackageDimension = process_registry.extend("reception", { } }, }, - "methods.prefill_packaging_form_inputs": function () { - if (!this.state_is("set_packaging_dimension")) return; + 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(); + 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]); - } - }); - }, - "methods.get_packaging_measurements_inputs": function () { - return [ - "packaging_length_input", - "width_input", - "height_input", - "weight_input", - "qty_input", - "barcode_input", - ]; - }, - "methods.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; + input_fields.forEach((inputKey) => { + const originalKey = inputKey.replace("_input", ""); + if (pkg[inputKey] === undefined || pkg[inputKey] === null) { + this.$set(pkg, inputKey, pkg[originalKey]); + } + }); + }, + 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: "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: "qty", + label: "Quantity", + klass: _is_field_changed("qty") ? "accent" : "", }, - }, - { - 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: "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: "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; + { + 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; + }, }, - }, - ], - }; - return options; - }, - "methods._get_states": function () { - let states = _get_states.bind(this)(); + { + 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; + // 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; - }, - on_skip: async function () { - const payload = self.state.get_payload_set_packaging_dimension(); - payload["skip"] = true; - await self.wait_call( - self.odoo.call("set_packaging_dimension", payload) - ); - self.prefill_packaging_form_inputs(); - }, - on_done: async function () { - const payload = self.state.get_payload_set_packaging_dimension(); - await self.wait_call( - self.odoo.call("set_packaging_dimension", payload) - ); - self.prefill_packaging_form_inputs(); - }, - }; - return states; + 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; + }, + on_skip: async function () { + const payload = self.state.get_payload_set_packaging_dimension(); + payload["skip"] = true; + await self.wait_call( + self.odoo.call("set_packaging_dimension", payload) + ); + self.prefill_packaging_form_inputs(); + }, + on_done: async function () { + const payload = self.state.get_payload_set_packaging_dimension(); + await self.wait_call( + self.odoo.call("set_packaging_dimension", payload) + ); + self.prefill_packaging_form_inputs(); + }, + }; + return states; + }, }, }); From dc6ee0d1e672dbdf14bb343c7a5340a93c3174fb Mon Sep 17 00:00:00 2001 From: Nicolas Delbovier Date: Fri, 24 Apr 2026 08:34:38 +0200 Subject: [PATCH 12/12] [IMP] shopfloor_reception_packaging_dimension_mobile: scroll to top on submission --- .../scenario/reception_packaging_dimension.js | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) 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 04edb4bc006..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 @@ -215,7 +215,7 @@ const ReceptionPackageDimension = process_registry.extend("reception", { events: { go_back: "on_back", }, - get_payload_set_packaging_dimension: () => { + _get_payload_set_packaging_dimension: () => { let values = { picking_id: this.state.data.picking.id, selected_line_id: this.state.data.selected_move_line.id, @@ -227,20 +227,27 @@ const ReceptionPackageDimension = process_registry.extend("reception", { } return values; }, - on_skip: async function () { - const payload = self.state.get_payload_set_packaging_dimension(); - payload["skip"] = true; + _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 () { - const payload = self.state.get_payload_set_packaging_dimension(); - await self.wait_call( - self.odoo.call("set_packaging_dimension", payload) - ); - self.prefill_packaging_form_inputs(); + await self.state._handle_dimension_submission(false); }, }; return states;