Skip to content

Commit

Permalink
[IMP] stock_picking_batch_creation: Allows to split picking
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lmignon committed Oct 10, 2024
1 parent b7a641e commit 4149398
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 35 deletions.
3 changes: 2 additions & 1 deletion stock_picking_batch_creation/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions stock_picking_batch_creation/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
90 changes: 86 additions & 4 deletions stock_picking_batch_creation/tests/test_clustering_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
86 changes: 58 additions & 28 deletions stock_picking_batch_creation/wizards/make_picking_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions stock_picking_batch_creation/wizards/make_picking_batch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
<field name="arch" type="xml">
<form string="Make Picking Batch">
<group>
<group string="Criteria" name="criteria" colspan="4">
<group string="Criteria" name="criteria" colspan="2">
<field name="user_id" />
<field name="picking_type_ids" widget="many2many_tags" />
<field name="maximum_number_of_preparation_lines" />
<field name="no_line_limit_if_no_candidate" />
</group>
<group name="devices">
<label for="stock_device_type_ids" colspan="4" />
Expand All @@ -29,6 +28,7 @@
<field name="group_pickings_by_partner" />
<field name="restrict_to_same_partner" />
<field name="restrict_to_same_priority" />
<field name="split_picking_exceeding_limits" />
</group>
<group string="Diagnostic" name="debug" colspan="4">
<field name="add_picking_list_in_error" />
Expand Down

0 comments on commit 4149398

Please sign in to comment.