From d6769e8e46c9773be9ff4e35c0a44cb96046e6a9 Mon Sep 17 00:00:00 2001
From: Mmequignon <matthieu@fwzte.xyz>
Date: Thu, 5 Sep 2024 11:15:11 +0200
Subject: [PATCH] stock_reception_screen_measuring_device: measure smaller
 packages

When goods are received, triggers measurements also for smaller
packagings that weren't ordered or received.

This behavior is not triggered when received packagings are not the same
as ordered packagings.
---
 .../__manifest__.py                           |   2 +-
 .../migrations/14.0.2.0.0/post-migration.py   |  25 ++
 .../migrations/14.0.2.0.0/pre-migration.py    |  29 ++
 .../models/stock_reception_screen.py          |  82 +++-
 .../tests/__init__.py                         |   1 +
 .../tests/fake_components.py                  |  14 +
 .../tests/fake_models.py                      |  14 +
 .../test_reception_screen_measurement.py      | 360 ++++++++++++++++++
 .../views/stock_reception_screen_view.xml     |  13 +-
 9 files changed, 525 insertions(+), 15 deletions(-)
 create mode 100644 stock_reception_screen_measuring_device/migrations/14.0.2.0.0/post-migration.py
 create mode 100644 stock_reception_screen_measuring_device/migrations/14.0.2.0.0/pre-migration.py
 create mode 100644 stock_reception_screen_measuring_device/tests/__init__.py
 create mode 100644 stock_reception_screen_measuring_device/tests/fake_components.py
 create mode 100644 stock_reception_screen_measuring_device/tests/fake_models.py
 create mode 100644 stock_reception_screen_measuring_device/tests/test_reception_screen_measurement.py

diff --git a/stock_reception_screen_measuring_device/__manifest__.py b/stock_reception_screen_measuring_device/__manifest__.py
index 6b7cd6bf04..32e8d7068f 100644
--- a/stock_reception_screen_measuring_device/__manifest__.py
+++ b/stock_reception_screen_measuring_device/__manifest__.py
@@ -4,7 +4,7 @@
     "name": "Stock Measuring Device on Reception Screen",
     "summary": "Allow to use a measuring device from a reception screen."
     "for packaging measurement",
-    "version": "14.0.1.0.0",
+    "version": "14.0.2.0.0",
     "category": "Warehouse",
     "author": "Camptocamp, Odoo Community Association (OCA)",
     "license": "AGPL-3",
diff --git a/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/post-migration.py b/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/post-migration.py
new file mode 100644
index 0000000000..58d38dca17
--- /dev/null
+++ b/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/post-migration.py
@@ -0,0 +1,25 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+import logging
+
+from odoo import SUPERUSER_ID, api
+
+_logger = logging.getLogger(__name__)
+
+
+def populate_new_field(env):
+    records_to_update = (
+        env["stock.reception.screen"].search([("current_step", "!=", "done")]).exists()
+    )
+    _logger.info(
+        "Set smaller_package_has_missing_dimensions on ongoing reception screens"
+    )
+    records_to_update._compute_smaller_package_has_missing_dimensions()
+
+
+def migrate(cr, version):
+    if not version:
+        return
+    env = api.Environment(cr, SUPERUSER_ID, {})
+    populate_new_field(env)
diff --git a/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/pre-migration.py b/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/pre-migration.py
new file mode 100644
index 0000000000..1f0ff7fcc8
--- /dev/null
+++ b/stock_reception_screen_measuring_device/migrations/14.0.2.0.0/pre-migration.py
@@ -0,0 +1,29 @@
+# Copyright 2021 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+def create_and_populate_new_fields(cr):
+    cr.execute(
+        """
+        ALTER TABLE stock_reception_screen
+        ADD COLUMN IF NOT EXISTS smaller_package_has_missing_dimensions BOOLEAN;
+        """
+    )
+    # Set value to False for done reception screens.
+    # Otherwise, let the ORM do its job in post migration
+    _logger.info("Set smaller_package_has_missing_dimensions on done reception screens")
+    cr.execute(
+        """
+        UPDATE stock_reception_screen
+        SET smaller_package_has_missing_dimensions = FALSE
+        WHERE current_step = 'done';
+        """
+    )
+
+
+def migrate(cr, version):
+    create_and_populate_new_fields(cr)
diff --git a/stock_reception_screen_measuring_device/models/stock_reception_screen.py b/stock_reception_screen_measuring_device/models/stock_reception_screen.py
index 443fd4a36e..bc6512da8a 100644
--- a/stock_reception_screen_measuring_device/models/stock_reception_screen.py
+++ b/stock_reception_screen_measuring_device/models/stock_reception_screen.py
@@ -18,6 +18,12 @@ class StockReceptionScreen(models.Model):
         store=True,
         help="Indicates if the package have any measurement missing.",
     )
+    smaller_package_has_missing_dimensions = fields.Boolean(
+        "Smaller Package Requires Measures?",
+        compute="_compute_smaller_package_has_missing_dimensions",
+        store=True,
+        help="Indicates if any smaller package have any measurement missing.",
+    )
     display_package_dimensions = fields.Char(
         string="Dimensions (lxhxw)",
         compute="_compute_package_dimensions",
@@ -30,13 +36,18 @@ class StockReceptionScreen(models.Model):
         store=True,
     )
 
-    @api.depends("product_packaging_id", "product_packaging_id.measuring_device_id")
+    @api.depends(
+        "current_move_product_id.packaging_ids.measuring_device_id",
+    )
     def _compute_scan_requested(self):
         for record in self:
-            record.scan_requested = (
-                record.product_packaging_id
-                and record.product_packaging_id.measuring_device_id
-            )
+            # TODO
+            all_product_packagings = record.current_move_product_id.packaging_ids
+            record.scan_requested = False
+            for packaging in all_product_packagings:
+                if packaging.measuring_device_id:
+                    record.scan_requested = True
+                    break
 
     @api.depends(
         "product_packaging_id.packaging_length",
@@ -54,6 +65,20 @@ def _compute_package_dimensions(self):
             else:
                 record.display_package_dimensions = False
 
+    @api.depends(
+        "product_packaging_id",
+        "product_packaging_id.qty",
+        "current_move_product_id.packaging_ids.max_weight",
+        "current_move_product_id.packaging_ids.packaging_length",
+        "current_move_product_id.packaging_ids.width",
+        "current_move_product_id.packaging_ids.height",
+    )
+    def _compute_smaller_package_has_missing_dimensions(self):
+        for record in self:
+            record.smaller_package_has_missing_dimensions = bool(
+                record._get_smaller_package_without_dimensions()
+            )
+
     @api.depends(
         "product_packaging_id.max_weight",
         "product_packaging_id.packaging_length",
@@ -71,8 +96,8 @@ def _compute_package_has_missing_dimensions(self):
             else:
                 record.package_has_missing_dimensions = False
 
-    def measure_current_packaging(self):
-        self.ensure_one()
+    @api.model
+    def _measure_packaging(self, packaging):
         device = self.env["measuring.device"].search(
             [("is_default", "=", True)], limit=1
         )
@@ -80,20 +105,53 @@ def measure_current_packaging(self):
             error_msg = _("No default device set, please configure one.")
             _logger.error(error_msg)
             self._notify(error_msg)
-            return UserError(error_msg)
+            raise UserError(error_msg)
         if device._is_being_used():
             error_msg = _("Measurement machine already in use.")
             _logger.error(error_msg)
             self._notify(error_msg)
-            return UserError(error_msg)
+            raise UserError(error_msg)
 
-        self.product_packaging_id._measuring_device_assign(device)
+        packaging._measuring_device_assign(device)
         return True
 
+    def measure_current_packaging(self):
+        self.ensure_one()
+        return self._measure_packaging(self.product_packaging_id)
+
+    def _get_smaller_package_without_dimensions_domain(self):
+        self.ensure_one()
+        return [
+            ("product_id", "=", self.current_move_product_id.id),
+            ("qty", "<", self.product_packaging_id.qty),
+            "|",
+            "|",
+            "|",
+            ("packaging_length", "=", 0),
+            ("width", "=", 0),
+            ("height", "=", 0),
+            ("max_weight", "=", 0),
+        ]
+
+    def _get_smaller_package_without_dimensions(self):
+        self.ensure_one()
+        domain = self._get_smaller_package_without_dimensions_domain()
+        return self.env["product.packaging"].search(domain, order="qty desc", limit=1)
+
+    def measure_smaller_packaging(self):
+        self.ensure_one()
+        pack_without_dimensions = self._get_smaller_package_without_dimensions()
+        if not pack_without_dimensions:
+            error_msg = _("No available packaging without measurements.")
+            raise UserError(error_msg)
+        return self._measure_packaging(pack_without_dimensions)
+
     def cancel_measure_current_packaging(self):
         self.ensure_one()
-        self.product_packaging_id._measuring_device_release()
-        return True
+        assigned_packaging = self.current_move_product_id.packaging_ids.filtered(
+            lambda p: p.measuring_device_id
+        )
+        assigned_packaging._measuring_device_release()
 
     def _notify(self, message):
         """Show a gentle notification on the wizard"""
diff --git a/stock_reception_screen_measuring_device/tests/__init__.py b/stock_reception_screen_measuring_device/tests/__init__.py
new file mode 100644
index 0000000000..7863dd1075
--- /dev/null
+++ b/stock_reception_screen_measuring_device/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_reception_screen_measurement
diff --git a/stock_reception_screen_measuring_device/tests/fake_components.py b/stock_reception_screen_measuring_device/tests/fake_components.py
new file mode 100644
index 0000000000..ba1cafde55
--- /dev/null
+++ b/stock_reception_screen_measuring_device/tests/fake_components.py
@@ -0,0 +1,14 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+from odoo.addons.component.core import Component
+
+
+class FakeDevice(Component):
+    _name = "device.component.fake"
+    _inherit = "measuring.device.base"
+    _usage = "fake"
+
+    def post_update_packaging_measures(self, measures, packaging, wizard_line):
+        # Unassign measuring device when measuring is done
+        packaging._measuring_device_release()
diff --git a/stock_reception_screen_measuring_device/tests/fake_models.py b/stock_reception_screen_measuring_device/tests/fake_models.py
new file mode 100644
index 0000000000..05b77f02dc
--- /dev/null
+++ b/stock_reception_screen_measuring_device/tests/fake_models.py
@@ -0,0 +1,14 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+
+from odoo import fields, models
+
+
+class FakeMeasuringDevice(models.Model):
+    _inherit = "measuring.device"
+
+    device_type = fields.Selection(selection_add=[("fake", "FAKE")])
+
+    def mocked_measure(self, measurements):
+        self.ensure_one()
+        self._update_packaging_measures(measurements)
diff --git a/stock_reception_screen_measuring_device/tests/test_reception_screen_measurement.py b/stock_reception_screen_measuring_device/tests/test_reception_screen_measurement.py
new file mode 100644
index 0000000000..eb0eccbc3e
--- /dev/null
+++ b/stock_reception_screen_measuring_device/tests/test_reception_screen_measurement.py
@@ -0,0 +1,360 @@
+# Copyright 2024 Camptocamp SA
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
+from odoo_test_helper import FakeModelLoader
+
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+
+from odoo.addons.component.tests.common import SavepointComponentRegistryCase
+
+from .fake_components import FakeDevice
+
+
+@tagged("-at_install", "post_install")
+class TestReceptionScreenMeasurement(SavepointComponentRegistryCase):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
+        cls.setUpModels()
+        cls.setUpClassStorageType()
+        cls.setUpClassProduct()
+        cls.location_dest = cls.env.ref("stock.stock_location_stock")
+        cls.location_src = cls.env.ref("stock.stock_location_suppliers")
+        cls.warehouse = cls.env.ref("stock.warehouse0")
+        cls.partner = cls.env.ref("base.res_partner_1")
+        cls.setUpClassMeasuringDevice()
+        cls.setUpClassComponents()
+
+    @classmethod
+    def setUpClassComponents(cls):
+        cls._setup_registry(cls)
+        cls._build_components(cls, FakeDevice)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.loader.restore_registry()
+        return super().tearDownClass()
+
+    @classmethod
+    def setUpModels(cls):
+        cls.loader = FakeModelLoader(cls.env, cls.__module__)
+        cls.loader.backup_registry()
+        from .fake_models import FakeMeasuringDevice
+
+        cls.loader.update_registry((FakeMeasuringDevice,))
+
+    @classmethod
+    def setUpClassMeasuringDevice(cls):
+        cls.measuring_device = cls.env["measuring.device"].create(
+            {
+                "name": "TestDevice",
+                "warehouse_id": cls.warehouse.id,
+                "device_type": "fake",
+                "is_default": True,
+            }
+        )
+
+    @classmethod
+    def setUpClassStorageType(cls):
+        cls.location_storage_model = cls.env["stock.location.storage.type"]
+        cls.location_boxes = cls.location_storage_model.create(
+            {
+                "name": "Boxes location",
+            }
+        )
+        cls.location_pallets = cls.location_storage_model.create(
+            {
+                "name": "Pallets location",
+            }
+        )
+        cls.storage_type_model = cls.env["stock.package.storage.type"]
+        cls.storage_type_box = cls.storage_type_model.create(
+            {
+                "name": "BOX",
+                "location_storage_type_ids": [(4, cls.location_boxes.id)],
+            }
+        )
+        cls.storage_type_box_of_boxes = cls.storage_type_model.create(
+            {
+                "name": "BOX_OF_BOXES",
+                "location_storage_type_ids": [(4, cls.location_boxes.id)],
+            }
+        )
+        cls.storage_type_pallet = cls.storage_type_model.create(
+            {
+                "name": "PALLET",
+                "location_storage_type_ids": [(4, cls.location_pallets.id)],
+            }
+        )
+
+    @classmethod
+    def setUpClassProduct(cls):
+        cls.product_screw = cls.env["product.product"].create(
+            {
+                "name": "SCREWS",
+                "type": "product",
+            }
+        )
+        cls.packaging_model = cls.env["product.packaging"]
+
+        # Set default dimensions to mimick what is done from the UI
+        # -> Numerical values evaluated to False are converted to 0.0
+        default_dimensions = {
+            "packaging_length": 0,
+            "width": 0,
+            "height": 0,
+            "max_weight": 0,
+        }
+        cls.packaging_smaller_box = cls.packaging_model.create(
+            dict(
+                {
+                    "name": "SMALLER BOX OF SCREWS",
+                    "product_id": cls.product_screw.id,
+                    "qty": 5,
+                    "package_storage_type_id": cls.storage_type_box.id,
+                },
+                **default_dimensions,
+            )
+        )
+        cls.packaging_regular_box = cls.packaging_model.create(
+            dict(
+                {
+                    "name": "REGULAR BOX OF SCREWS",
+                    "product_id": cls.product_screw.id,
+                    "qty": 50,
+                    "package_storage_type_id": cls.storage_type_box.id,
+                },
+                **default_dimensions,
+            )
+        )
+        cls.packaging_huge_box = cls.packaging_model.create(
+            dict(
+                {
+                    "name": "HUGE BOX OF SCREWS",
+                    "product_id": cls.product_screw.id,
+                    "qty": 500,
+                    "package_storage_type_id": cls.storage_type_box_of_boxes.id,
+                },
+                **default_dimensions,
+            )
+        )
+        cls.packaging_pallet = cls.packaging_model.create(
+            dict(
+                {
+                    "name": "PALLET OF SCREWS",
+                    "product_id": cls.product_screw.id,
+                    "qty": 5000,
+                    "package_storage_type_id": cls.storage_type_pallet.id,
+                    "type_is_pallet": True,
+                },
+                **default_dimensions,
+            )
+        )
+        cls.all_packages_no_pallet = (
+            cls.packaging_smaller_box
+            | cls.packaging_regular_box
+            | cls.packaging_huge_box
+        )
+        cls.all_packages = cls.all_packages_no_pallet | cls.packaging_pallet
+
+    @classmethod
+    def _create_picking_get_move_vals(cls, product_matrix):
+        move_vals = []
+        defaults = {
+            "location_id": cls.location_src.id,
+            "location_dest_id": cls.location_dest.id,
+        }
+        for product, qty in product_matrix:
+            product_vals = {
+                "product_id": product.id,
+                "name": product.name,
+                "product_uom_qty": qty,
+                "product_uom": product.uom_id.id,
+            }
+            move_vals.append((0, 0, dict(defaults, **product_vals)))
+        return move_vals
+
+    @classmethod
+    def _create_picking_get_values(cls, product_matrix):
+        return {
+            "partner_id": cls.partner.id,
+            "location_id": cls.location_src.id,
+            "location_dest_id": cls.location_dest.id,
+            "picking_type_id": cls.env.ref("stock.picking_type_in").id,
+            "move_lines": cls._create_picking_get_move_vals(product_matrix),
+        }
+
+    @classmethod
+    def _create_picking_in(cls, product_matrix):
+        picking_values = cls._create_picking_get_values(product_matrix)
+        return cls.env["stock.picking"].create(picking_values)
+
+    @classmethod
+    def _picking_get_reception_screen(cls, picking):
+        picking.action_confirm()
+        picking.action_reception_screen_open()
+        return picking.reception_screen_id
+
+    @classmethod
+    def _packaging_flush_dimensions(cls, packagings):
+        field_names = ["max_weight", "packaging_length", "width", "height"]
+        packagings.write({key: 0.0 for key in field_names})
+
+    @classmethod
+    def _packaging_get_default_dimensions(cls):
+        field_names = ["max_weight", "packaging_length", "width", "height"]
+        return {key: 42 for key in field_names}
+
+    @classmethod
+    def _packaging_set_dimensions(cls, packagings):
+        packagings.write(cls._packaging_get_default_dimensions())
+
+    def get_screen_at_packaging_selection(self, picking):
+        move_screw = picking.move_lines[0]
+        reception_screen = self._picking_get_reception_screen(picking)
+        self.assertEqual(reception_screen.current_step, "select_product")
+        move_screw.action_select_product()
+        # Product isn't tracked by serial, next step is set_quantity
+        self.assertEqual(reception_screen.current_step, "set_quantity")
+        # receiving 800 out of 1000.0
+        reception_screen.current_move_line_qty_done = 800
+        self.assertEqual(reception_screen.current_move_line_qty_status, "lt")
+        # Check package data (automatically filled normally)
+        reception_screen.button_save_step()
+        self.assertEqual(reception_screen.current_step, "select_packaging")
+        return reception_screen
+
+    def test_current_package_needs_measurement(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        # No dimension is set on the selected package
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        self.assertTrue(reception_screen.package_has_missing_dimensions)
+        # Setting dimensions sets the package_has_missing_dimensions to False
+        self._packaging_set_dimensions(self.packaging_huge_box)
+        reception_screen.invalidate_cache(["package_has_missing_dimensions"])
+        self.assertFalse(reception_screen.package_has_missing_dimensions)
+
+    def test_smaller_package_needs_measurement(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        # Select bigger package, set measurements, package_has_missing_dimensions
+        # is set to False
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        self._packaging_set_dimensions(self.packaging_huge_box)
+        self.assertFalse(reception_screen.package_has_missing_dimensions)
+        # However, regular and smaller boxes are missing measurements
+        # smaller_package_has_missing_dimensions should be set to True
+        self.assertTrue(reception_screen.smaller_package_has_missing_dimensions)
+        # Set dimensions on regular box, smaller box is still missing dimensions
+        self._packaging_set_dimensions(self.packaging_regular_box)
+        reception_screen.invalidate_cache(["smaller_package_has_missing_dimensions"])
+        self.assertTrue(reception_screen.smaller_package_has_missing_dimensions)
+        # Set dimensions on smaller box, all dimensions are set on packages
+        # smaller than the selected one
+        self._packaging_set_dimensions(self.packaging_smaller_box)
+        reception_screen.invalidate_cache(["smaller_package_has_missing_dimensions"])
+        self.assertFalse(reception_screen.smaller_package_has_missing_dimensions)
+
+    def test_measure_from_biggest_packaging_to_smallest(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        # select biggest package
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        # Click the measure current package button
+        reception_screen.measure_current_packaging()
+        # Measuring device is assigned to the current package
+        self.assertEqual(
+            self.packaging_huge_box.measuring_device_id, self.measuring_device
+        )
+        # set dimensions on packaging using mocked measuring device
+        measurement_values = {
+            k: 42 for k in ("max_weight", "packaging_length", "width", "height")
+        }
+        self.measuring_device.mocked_measure(measurement_values)
+        # Now measuring device is unassigned
+        self.assertFalse(self.packaging_huge_box.measuring_device_id)
+        # and all measurements are set on packaging
+        for key, value in measurement_values.items():
+            self.assertEqual(self.packaging_huge_box[key], value)
+        # There's still smaller packages missing measurements
+        self.assertTrue(reception_screen.smaller_package_has_missing_dimensions)
+        # Click the measure smaller package button
+        reception_screen.measure_smaller_packaging()
+        # Among the 2 smaller packagings without dimension,
+        # the biggest has the priority and should have been selected
+        self.assertEqual(
+            self.packaging_regular_box.measuring_device_id, self.measuring_device
+        )
+        # Set quantity, we still have a smaller packaging without measurement
+        self.measuring_device.mocked_measure(measurement_values)
+        for key, value in measurement_values.items():
+            self.assertEqual(self.packaging_regular_box[key], value)
+        reception_screen.invalidate_cache(["smaller_package_has_missing_dimensions"])
+        self.assertTrue(reception_screen.smaller_package_has_missing_dimensions)
+        # Select next smaller packaging for measurement, the smallest should be selected
+        reception_screen.measure_smaller_packaging()
+        self.assertEqual(
+            self.packaging_smaller_box.measuring_device_id, self.measuring_device
+        )
+        # Set quantity, no more smaller packaging to measure,
+        # smaller_package_has_missing_dimensions should be False
+        self.measuring_device.mocked_measure(measurement_values)
+        for key, value in measurement_values.items():
+            self.assertEqual(self.packaging_smaller_box[key], value)
+        reception_screen.invalidate_cache(["smaller_package_has_missing_dimensions"])
+        self.assertFalse(reception_screen.smaller_package_has_missing_dimensions)
+
+    def test_measuring_device_skips_measured_packagings(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        # Set dimensions on bigger and regular box
+        self._packaging_set_dimensions(
+            self.packaging_huge_box | self.packaging_regular_box
+        )
+        self.assertFalse(reception_screen.package_has_missing_dimensions)
+        self.assertTrue(reception_screen.smaller_package_has_missing_dimensions)
+        # Selected packaging for measurement should be packaging_smaller_box
+        reception_screen.measure_smaller_packaging()
+        self.assertEqual(
+            self.packaging_smaller_box.measuring_device_id, self.measuring_device
+        )
+
+    def test_measurement_device_cancel_package_measurement(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        # Select huge packaging
+        reception_screen.measure_current_packaging()
+        self.assertTrue(self.packaging_huge_box.measuring_device_id)
+        reception_screen.cancel_measure_current_packaging()
+        self.assertFalse(self.packaging_huge_box.measuring_device_id)
+        # Assigning measuring device to regular packaging then pressing the cancel
+        # should unassign the measuring device
+        reception_screen.measure_smaller_packaging()
+        self.assertTrue(self.packaging_regular_box.measuring_device_id)
+        reception_screen.cancel_measure_current_packaging()
+        self.assertFalse(self.packaging_regular_box.measuring_device_id)
+
+    def test_measuring_device_cannot_be_assigned_twice(self):
+        picking = self._create_picking_in([(self.product_screw, 1000.0)])
+        picking.action_confirm()
+        reception_screen = self.get_screen_at_packaging_selection(picking)
+        # Select huge box as packaging
+        reception_screen.product_packaging_id = self.packaging_huge_box
+        # Assign device to huge box packaging
+        reception_screen.measure_current_packaging()
+        self.assertTrue(self.packaging_huge_box.measuring_device_id)
+        # Then try to assign device to regular box packaging, and check
+        # that it had no effect
+        message = r"Measurement machine already in use."
+        with self.assertRaisesRegex(UserError, message):
+            reception_screen.measure_smaller_packaging()
+        self.assertFalse(self.packaging_regular_box.measuring_device_id)
diff --git a/stock_reception_screen_measuring_device/views/stock_reception_screen_view.xml b/stock_reception_screen_measuring_device/views/stock_reception_screen_view.xml
index 04a3986cd1..eabfbf42ef 100644
--- a/stock_reception_screen_measuring_device/views/stock_reception_screen_view.xml
+++ b/stock_reception_screen_measuring_device/views/stock_reception_screen_view.xml
@@ -12,15 +12,24 @@
         <field name="arch" type="xml">
             <button name="button_next_pack" position="after">
                 <field name="package_has_missing_dimensions" invisible="1" />
+                <field name="smaller_package_has_missing_dimensions" invisible="1" />
+                <field name="scan_requested" invisible="1" />
                 <button
                     name="measure_current_packaging"
                     type="object"
                     string="Measure Current Packaging"
                     icon="fa-tachometer"
                     class="btn btn-secondary"
-                    attrs="{'invisible': [('package_has_missing_dimensions', '=', False)]}"
+                    attrs="{'invisible': ['|', ('package_has_missing_dimensions', '=', False), ('scan_requested', '=', True)]}"
+                />
+                <button
+                    name="measure_smaller_packaging"
+                    type="object"
+                    string="Measure Smaller Packaging"
+                    icon="fa-tachometer"
+                    class="btn btn-secondary"
+                    attrs="{'invisible': ['|', '|', ('package_has_missing_dimensions', '=', True), ('smaller_package_has_missing_dimensions', '=', False), ('scan_requested', '=', True)]}"
                 />
-                <field name="scan_requested" invisible="1" />
                 <button
                     name="cancel_measure_current_packaging"
                     type="object"