From 41493986bf6c5b5f6f6132f7e4a2b6c5cae5b28b Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Thu, 10 Oct 2024 11:58:32 +0200
Subject: [PATCH 1/3] [IMP] stock_picking_batch_creation: Allows to split
picking
A new option 'Split picking exceeding the limits' on the wizard 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.
---
stock_picking_batch_creation/__manifest__.py | 3 +-
stock_picking_batch_creation/readme/USAGE.rst | 10 +++
.../tests/test_clustering_conditions.py | 90 ++++++++++++++++++-
.../wizards/make_picking_batch.py | 86 ++++++++++++------
.../wizards/make_picking_batch.xml | 4 +-
5 files changed, 158 insertions(+), 35 deletions(-)
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 @@
+
+
+
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.
+
-
+
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 @@
Do not contact contributors directly about support or help with technical issues.
-
+
-
+
The development of this module has been financially supported by:
- ACSONE SA/NV
@@ -517,9 +533,11 @@
-
+
This module is maintained by the OCA.
-
+
+
+
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 b15dac032b..cb513cfbb3 100644
--- a/stock_picking_batch_creation/tests/test_clustering_conditions.py
+++ b/stock_picking_batch_creation/tests/test_clustering_conditions.py
@@ -532,3 +532,53 @@ def test_picking_split_with_volume_exceed(self):
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)
diff --git a/stock_picking_batch_creation/wizards/make_picking_batch.py b/stock_picking_batch_creation/wizards/make_picking_batch.py
index ac45e0d02f..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):
@@ -279,7 +282,29 @@ def _split_first_picking_for_limit(self, picking):
).action_apply()
return picking
- def _get_first_picking(self, no_limit=False):
+ 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
+ )
+
+ 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 = []
@@ -287,11 +312,21 @@ def _get_first_picking(self, no_limit=False):
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)
+ 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):
@@ -376,22 +411,22 @@ 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()
+ 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(pickings=first_picking)
+ raise NoSuitableDeviceError(self.env, pickings=first_picking)
return self.env["stock.picking.batch"].browse()
self._init_counters(first_picking, device)
self._apply_limits()