diff --git a/stock_picking_batch_creation/README.rst b/stock_picking_batch_creation/README.rst index 1f349d7e86..a4123e3c62 100644 --- a/stock_picking_batch_creation/README.rst +++ b/stock_picking_batch_creation/README.rst @@ -140,6 +140,23 @@ 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. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can't be split, an exception will be raised. + +This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first. + Bug Tracker =========== 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/exceptions.py b/stock_picking_batch_creation/exceptions.py index 430648b561..0b6b8d1580 100644 --- a/stock_picking_batch_creation/exceptions.py +++ b/stock_picking_batch_creation/exceptions.py @@ -6,7 +6,8 @@ class NoPickingCandidateError(UserError): - def __init__(self): + def __init__(self, env): + self.env = env super(NoPickingCandidateError, self).__init__( _("no candidate pickings to batch") ) @@ -14,6 +15,7 @@ def __init__(self): class PickingCandidateNumberLineExceedError(UserError): def __init__(self, picking, max_line): + self.env = picking.env self.picking = picking super(PickingCandidateNumberLineExceedError, self).__init__( _( @@ -28,7 +30,8 @@ def __init__(self, picking, max_line): class NoSuitableDeviceError(UserError): - def __init__(self, pickings): + def __init__(self, env, pickings): + self.env = env self.pickings = pickings message = _("No device found for batch picking.") if pickings: @@ -37,3 +40,12 @@ def __init__(self, pickings): names=", ".join(self.pickings.mapped("name")), ) super(NoSuitableDeviceError, self).__init__(message) + + +class PickingSplitNotPossibleError(UserError): + def __init__(self, picking): + self.env = picking.env + self.picking = picking + super(PickingSplitNotPossibleError, self).__init_( + _("Picking %(name)s cannot be split", name=self.picking.name) + ) diff --git a/stock_picking_batch_creation/readme/USAGE.rst b/stock_picking_batch_creation/readme/USAGE.rst index 3c67061e1f..c27f24e170 100644 --- a/stock_picking_batch_creation/readme/USAGE.rst +++ b/stock_picking_batch_creation/readme/USAGE.rst @@ -66,3 +66,20 @@ 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. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can't be split, an exception will be raised. + +This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first. \ No newline at end of file diff --git a/stock_picking_batch_creation/static/description/index.html b/stock_picking_batch_creation/static/description/index.html index c42de5c9b9..a7183aa0d0 100644 --- a/stock_picking_batch_creation/static/description/index.html +++ b/stock_picking_batch_creation/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -409,16 +409,17 @@

Stock device types

  • Advanced configuration
  • -
  • Bug Tracker
  • -
  • Credits @@ -483,10 +484,25 @@

    Grouping by partner

    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. In this case, when the system select the first picking to add to the +batch, it will disable the criteria based on the volume, weight and number of +lines. If the picking is exceeding the limits, the system will then try to split +the picking so that the new picking fits the criteria and can be added to the +batch. If the picking can’t be split, an exception will be raised.

    +

    This option is useful to allow to create a batch picking with pickings that +are exceeding the limits defined in the wizard. It also ensures that the +processing of pickings is done in the order of the pickings. If the option is +not activated, the system will try to find a picking that fits the criteria +and will ignore those that are exceeding the limits even if they are to be +processed first.

    +
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -494,22 +510,22 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by:

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    diff --git a/stock_picking_batch_creation/tests/test_clustering_conditions.py b/stock_picking_batch_creation/tests/test_clustering_conditions.py index 73d2313e14..cb513cfbb3 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,144 @@ 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), 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) + + def test_picking_split_priority(self): + # We ensure than even if a picking with a higher priority has a volume + # exceeding the device capacity, it will be split and processed first + # if the split_picking_exceeding_limits is set to True + # the processing order for picks of type 1 will be: + # pick3 (priority), pick1 (lower id), pick2 + 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() + + # since pick3 exceeds the device capacity and + # the split_picking_exceeding_limits is set to False + # the next picking to process should be pick1 + batch = self.make_picking_batch._create_batch() + self.assertEqual(self.pick1, batch.picking_ids) + + batch.unlink() + + # if the split_picking_exceeding_limits is set to True. + # then pick3 should be split and processed first + 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) diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.py b/stock_picking_batch_creation/wizards/make_picking_batch.py index 9c4e445558..94653aaf17 100644 --- a/stock_picking_batch_creation/wizards/make_picking_batch.py +++ b/stock_picking_batch_creation/wizards/make_picking_batch.py @@ -1,6 +1,6 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - +import logging import math import threading from collections import defaultdict @@ -13,8 +13,11 @@ NoPickingCandidateError, NoSuitableDeviceError, PickingCandidateNumberLineExceedError, + PickingSplitNotPossibleError, ) +_logger = logging.getLogger(__name__) + class MakePickingBatch(models.TransientModel): @@ -80,16 +83,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 +261,73 @@ 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 + 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 _is_picking_exceeding_limits(self, picking): + """Check if the picking exceeds the limits of the available devices. + + :param picking: the picking to check + """ + nbr_lines, volume, weight = self._get_picking_max_dimensions() + return ( + picking.nbr_picking_lines > nbr_lines + or picking.volume > volume + or picking.weight > weight ) - 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_first_picking(self, raise_if_not_found=False): + """Get the first picking to add to the batch. + + If the split_picking_exceeding_limits is set, we try to find the first picking + without taking into account the limit on the number of lines and we split it + if it exceeds the limits. If the split is not possible, we raise an error. + + Otherwise, we try to find the first picking taking into account the limit on the + number of lines. + """ + no_limit = self.split_picking_exceeding_limits + 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 not picking and not no_limit and raise_if_not_found: + self._raise_create_batch_not_possible() + # at this stage we have the first picking to add to the batch but it could + # exceed the limits of the available devices. In this case we split the + # picking and return the picking to add to the batch. The split is done only + # if the split_picking_exceeding_limits is set to True. + if ( + picking + and self.split_picking_exceeding_limits + and self._is_picking_exceeding_limits(picking) + ): + split_picking = self._split_first_picking_for_limit(picking) + if not split_picking and raise_if_not_found: + raise PickingSplitNotPossibleError(picking) + picking = split_picking + return picking def _get_additional_picking(self): """Get the next picking to add to the batch.""" @@ -334,13 +393,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: @@ -348,21 +411,23 @@ def _raise_create_batch_not_possible(self): candidates = self.env["stock.picking"].search(domain, limit=limit) if candidates: pickings = candidates if self.add_picking_list_in_error else None - raise NoSuitableDeviceError(pickings=pickings) - raise NoPickingCandidateError() + raise NoSuitableDeviceError(self.env, pickings=pickings) + raise NoPickingCandidateError(self.env) def _create_batch(self, raise_if_not_possible=False): """Create a batch transfer.""" 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) + first_picking = self._get_first_picking( + raise_if_not_found=raise_if_not_possible + ) 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(self.env, 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 @@
    - + - diff --git a/test-requirements.txt b/test-requirements.txt index 689482e20d..05c40aa45b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,3 @@ vcrpy-unittest odoo_test_helper +odoo-addon-stock-split-picking-dimension @ git+https://github.com/OCA/stock-logistics-workflow.git@refs/pull/1741/head#subdirectory=setup/stock_split_picking_dimension