diff --git a/stock_picking_batch_creation/__manifest__.py b/stock_picking_batch_creation/__manifest__.py index 8335457cc4..947d825195 100644 --- a/stock_picking_batch_creation/__manifest__.py +++ b/stock_picking_batch_creation/__manifest__.py @@ -5,7 +5,7 @@ "name": "Stock Picking Batch Creation", "summary": """ Create a batch of pickings to be processed all together""", - "version": "16.0.1.0.0", + "version": "16.0.2.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/wms", @@ -16,6 +16,7 @@ "delivery", # weight on picking "stock_picking_batch", "stock_picking_volume", # OCA/stock-logistics-warehouse + "stock_split_picking_dimension", # OCA/stock-logistics-workflow ], "data": [ "views/stock_device_type.xml", diff --git a/stock_picking_batch_creation/readme/USAGE.rst b/stock_picking_batch_creation/readme/USAGE.rst index 3c67061e1f..a227e47a36 100644 --- a/stock_picking_batch_creation/readme/USAGE.rst +++ b/stock_picking_batch_creation/readme/USAGE.rst @@ -66,3 +66,13 @@ will prevent to consume at least one bin for each picking if pickings are for the same partner. When activated, the computation of the number of bins consumed by the picking into the batch will take into account the volume of the pickings for the same partners already. + +Splitting picking if needed +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can also activate the option *Split picking exceeding the limits* on the +wizard. This will allow, when the system is not able to find a picking that +fits the criteria to create the batch, to lower the criteria by removing those +based on the volume, weight and number of lines. If a picking is found and +you allow to split it, the system will try to split the picking so that the +new picking fits the criteria and can be added to the batch. \ No newline at end of file diff --git a/stock_picking_batch_creation/tests/test_clustering_conditions.py b/stock_picking_batch_creation/tests/test_clustering_conditions.py index 73d2313e14..b15dac032b 100644 --- a/stock_picking_batch_creation/tests/test_clustering_conditions.py +++ b/stock_picking_batch_creation/tests/test_clustering_conditions.py @@ -432,7 +432,7 @@ def test_pickings_with_different_partners(self): batch2 = self.make_picking_batch._create_batch() self.assertEqual(self.pick1 | self.pick2, batch2.picking_ids) - def test_picking_with_maximum_number_of_lines_exceed(self): + def test_picking_split_with_maximum_number_of_lines_exceed(self): # pick 3 has 2 lines # create a batch picking with maximum number of lines = 1 self.pick1.action_cancel() @@ -441,12 +441,94 @@ def test_picking_with_maximum_number_of_lines_exceed(self): self.make_picking_batch.write( { "maximum_number_of_preparation_lines": 1, - "no_line_limit_if_no_candidate": False, + "split_picking_exceeding_limits": False, } ) with self.assertRaises(PickingCandidateNumberLineExceedError): self.make_picking_batch._create_batch(raise_if_not_possible=True) - self.make_picking_batch.no_line_limit_if_no_candidate = True + self.make_picking_batch.split_picking_exceeding_limits = True batch = self.make_picking_batch._create_batch() self.assertEqual(self.pick3, batch.picking_ids) - self.assertEqual(len(batch.move_line_ids), 2) + self.assertEqual(len(batch.move_line_ids), 1) + + def test_picking_split_with_weight_exceed(self): + # pick 3 has 2 lines + # we will set a weight by line under the maximum weight of the device + # but the total weight of the picking will exceed the maximum weight of the device + # when the batch is created, the picking 3 should be split and the batch + # should contain only pick3 with 1 line + + self.pick1.action_cancel() + self.pick2.action_cancel() + self.assertEqual(len(self.pick3.move_line_ids), 2) + max_weight = 200 + device = self.env["stock.device.type"].create( + { + "name": "test volume null devices and one bin", + "min_volume": 0, + "max_volume": 200, + "max_weight": max_weight, + "nbr_bins": 1, + "sequence": 50, + } + ) + + self.make_picking_batch.write( + { + "split_picking_exceeding_limits": False, + "stock_device_type_ids": [(6, 0, [device.id])], + } + ) + self.pick3.move_ids.product_id.weight = max_weight - 1 + self.pick3.move_ids._cal_move_weight() + with self.assertRaises(NoSuitableDeviceError): + self.make_picking_batch._create_batch(raise_if_not_possible=True) + self.make_picking_batch.split_picking_exceeding_limits = True + batch = self.make_picking_batch._create_batch() + self.assertEqual(self.pick3, batch.picking_ids) + self.assertEqual(len(batch.move_line_ids), 1) + + def test_picking_split_with_volume_exceed(self): + # pick 3 has 2 lines + # we will set a volume by line under the maximum volume of the device + # but the total volume of the picking will exceed the maximum volume of the device + # when the batch is created, the picking 3 should be split and the batch + # should contain only pick3 with 1 line + + self.pick1.action_cancel() + self.pick2.action_cancel() + self.assertEqual(len(self.pick3.move_line_ids), 2) + + max_volume = 200 + device = self.env["stock.device.type"].create( + { + "name": "test volume null devices and one bin", + "min_volume": 0, + "max_volume": max_volume, + "max_weight": 300, + "nbr_bins": 1, + "sequence": 50, + } + ) + + self.make_picking_batch.write( + { + "split_picking_exceeding_limits": False, + "stock_device_type_ids": [(6, 0, [device.id])], + } + ) + # each product has a volume of 120 + self.pick3.move_ids.product_id.write( + { + "product_length": 12, + "product_height": 5, + "product_width": 2, + } + ) + self.pick3.move_ids._compute_volume() + with self.assertRaises(NoSuitableDeviceError): + self.make_picking_batch._create_batch(raise_if_not_possible=True) + self.make_picking_batch.split_picking_exceeding_limits = True + batch = self.make_picking_batch._create_batch() + self.assertEqual(self.pick3, batch.picking_ids) + self.assertEqual(len(batch.move_line_ids), 1) diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.py b/stock_picking_batch_creation/wizards/make_picking_batch.py index 9c4e445558..ac45e0d02f 100644 --- a/stock_picking_batch_creation/wizards/make_picking_batch.py +++ b/stock_picking_batch_creation/wizards/make_picking_batch.py @@ -80,16 +80,14 @@ class MakePickingBatch(models.TransientModel): "by default.", ) - no_line_limit_if_no_candidate = fields.Boolean( - default=True, - string="No line limit if no candidate", - help="If checked, the maximum number of lines will not be applied if there is " - "no candidate to add to the batch with a number of lines less than the maximum " - "number of lines. This option is useful if you want relax the maximum number " - "of lines to allow to create a batch even if there is no candidate to add to " - "the batch at first. This will avoid to manually create a batch with a single " - "picking for the sole case where a device is suitable for the picking but the " - "picking has more lines than the maximum number of lines.", + split_picking_exceeding_limits = fields.Boolean( + default=False, + string="Split pickings exceeding limits", + help="If checked, the pickings exceeding the maximum number of lines, " + "volume or weight of available devices will be split into multiple pickings " + "to respect the limits. If unchecked, the pickings exceeding the limits will not " + "be added to the batch. The limits are defined by the limits of the last available " + "devices.", ) __slots__ = ( @@ -260,15 +258,41 @@ def _execute_search_pickings(self, domain, limit=None): domain, order=self._get_picking_order_by(), limit=limit ) - def _get_first_picking(self, no_nbr_lines_limit=False): - domain = self._get_picking_domain_for_first( - no_nbr_lines_limit=no_nbr_lines_limit - ) - device_domains = [] - for device in self.stock_device_type_ids: - device_domains.append(self._get_picking_domain_for_device(device)) - domain = AND([domain, OR(device_domains)]) - return self._execute_search_pickings(domain, limit=1) + def _get_picking_max_dimensions(self): + self.ensure_one() + nbr_lines = self.maximum_number_of_preparation_lines + last_device = self.stock_device_type_ids[-1] + volume = last_device.max_volume + weight = last_device.max_weight + return nbr_lines, volume, weight + + def _split_first_picking_for_limit(self, picking): + nbr_lines, volume, weight = self._get_picking_max_dimensions() + wizard = self.env["stock.split.picking"].with_context(active_ids=picking.ids) + wizard.create( + { + "mode": "dimensions", + "max_nbr_lines": nbr_lines, + "max_volume": volume, + "max_weight": weight, + } + ).action_apply() + return picking + + def _get_first_picking(self, no_limit=False): + domain = self._get_picking_domain_for_first(no_nbr_lines_limit=no_limit) + if not no_limit: + device_domains = [] + for device in self.stock_device_type_ids: + device_domains.append(self._get_picking_domain_for_device(device)) + domain = AND([domain, OR(device_domains)]) + picking = self._execute_search_pickings(domain, limit=1) + if self.split_picking_exceeding_limits: + if not no_limit and not picking: + return self._get_first_picking(no_limit=True) + if no_limit and picking: + return self._split_first_picking_for_limit(picking) + return picking def _get_additional_picking(self): """Get the next picking to add to the batch.""" @@ -334,13 +358,17 @@ def _raise_create_batch_not_possible(self): # constrains. If not, we raise an error to inform the user that there # is no picking to process otherwise we raise an error to inform the # user that there is not suitable device to process the pickings. - if not self.no_line_limit_if_no_candidate: - domain = self._get_picking_domain_for_first(no_nbr_lines_limit=True) - candidates = self.env["stock.picking"].search(domain, limit=1) - if candidates: - raise PickingCandidateNumberLineExceedError( - candidates, self.maximum_number_of_preparation_lines - ) + domain = self._get_picking_domain_for_first(no_nbr_lines_limit=True) + device_domains = [] + for device in self.stock_device_type_ids: + device_domains.append(self._get_picking_domain_for_device(device)) + domain = AND([domain, OR(device_domains)]) + candidates = self.env["stock.picking"].search(domain, limit=1) + if candidates: + raise PickingCandidateNumberLineExceedError( + candidates, self.maximum_number_of_preparation_lines + ) + domain = self._get_picking_domain_for_first() limit = 1 if self.add_picking_list_in_error: @@ -356,13 +384,15 @@ def _create_batch(self, raise_if_not_possible=False): self._reset_counters() # first we try to get the first picking for the user first_picking = self._get_first_picking() - if not first_picking and self.no_line_limit_if_no_candidate: - first_picking = self._get_first_picking(no_nbr_lines_limit=True) if not first_picking: if raise_if_not_possible: self._raise_create_batch_not_possible() return self.env["stock.picking.batch"].browse() device = self._compute_device_to_use(first_picking) + if not device: + if raise_if_not_possible: + raise NoSuitableDeviceError(pickings=first_picking) + return self.env["stock.picking.batch"].browse() self._init_counters(first_picking, device) self._apply_limits() vals = self._create_batch_values() diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.xml b/stock_picking_batch_creation/wizards/make_picking_batch.xml index c28c1fb197..6cd8962406 100644 --- a/stock_picking_batch_creation/wizards/make_picking_batch.xml +++ b/stock_picking_batch_creation/wizards/make_picking_batch.xml @@ -10,11 +10,10 @@
- + -