diff --git a/addons/stock_picking_batch/i18n/stock_picking_batch.pot b/addons/stock_picking_batch/i18n/stock_picking_batch.pot
index 76f0430811e..2d1c082e77d 100644
--- a/addons/stock_picking_batch/i18n/stock_picking_batch.pot
+++ b/addons/stock_picking_batch/i18n/stock_picking_batch.pot
@@ -707,14 +707,6 @@ msgstr ""
msgid "Show all records which has next action date is before today"
msgstr ""
-#. module: stock_picking_batch
-#: code:addons/stock_picking_batch/models/stock_picking_batch.py:0
-#, python-format
-msgid ""
-"Some transfers are still waiting for goods. Please check or force their "
-"availability before setting this batch to done."
-msgstr ""
-
#. module: stock_picking_batch
#: model:ir.model.fields,field_description:stock_picking_batch.field_stock_picking_type__batch_group_by_src_loc
msgid "Source Location"
diff --git a/addons/stock_picking_batch/models/stock_move.py b/addons/stock_picking_batch/models/stock_move.py
index 6cd617e9193..af4b61970f7 100644
--- a/addons/stock_picking_batch/models/stock_move.py
+++ b/addons/stock_picking_batch/models/stock_move.py
@@ -12,3 +12,12 @@ class StockMove(models.Model):
domain = super()._search_picking_for_assignation_domain()
domain = expression.AND([domain, ['|', ('batch_id', '=', False), ('batch_id.is_wave', '=', False)]])
return domain
+
+ def _action_cancel(self):
+ res = super()._action_cancel()
+
+ for picking in self.picking_id:
+ # Remove the picking from the batch if the whole batch isn't cancelled.
+ if picking.state == 'cancel' and picking.batch_id and any(p.state != 'cancel' for p in picking.batch_id.picking_ids):
+ picking.batch_id = None
+ return res
diff --git a/addons/stock_picking_batch/models/stock_picking.py b/addons/stock_picking_batch/models/stock_picking.py
index ca743febc4a..e09148e0de4 100644
--- a/addons/stock_picking_batch/models/stock_picking.py
+++ b/addons/stock_picking_batch/models/stock_picking.py
@@ -117,15 +117,27 @@ class StockPicking(models.Model):
def action_confirm(self):
res = super().action_confirm()
for picking in self:
- if picking.picking_type_id.auto_batch and not picking.immediate_transfer and not picking.batch_id and picking.move_ids and picking._is_auto_batchable():
- picking._find_auto_batch()
+ picking._find_auto_batch()
return res
def _action_done(self):
res = super()._action_done()
+ to_assign_ids = set()
+ if self and self.env.context.get('pickings_to_detach'):
+ self.env['stock.picking'].browse(self.env.context['pickings_to_detach']).batch_id = False
+ to_assign_ids.update(self.env.context['pickings_to_detach'])
+
for picking in self:
- if picking.batch_id and any(picking.state != 'done' for picking in picking.batch_id.picking_ids):
+ # Avoid inconsistencies in states of the same batch when validating a single picking in a batch.
+ if picking.batch_id and any(p.state != 'done' for p in picking.batch_id.picking_ids):
picking.batch_id = None
+ # If backorder were made, if auto-batch is enabled, seek a batch for each of them with the selected criterias.
+ to_assign_ids.update(picking.backorder_ids.ids)
+
+ # To avoid inconsistencies, all incorrect pickings must be removed before assigning backorder pickings
+ assignable_pickings = self.env['stock.picking'].browse(to_assign_ids)
+ for picking in assignable_pickings:
+ picking._find_auto_batch()
return res
@@ -137,11 +149,16 @@ class StockPicking(models.Model):
return res
def _should_show_transfers(self):
- if len(self.batch_id) == 1 and self == self.batch_id.picking_ids:
+ if len(self.batch_id) == 1 and len(self) == (len(self.batch_id.picking_ids) - len(self.env.context.get('pickings_to_detach', []))):
return False
return super()._should_show_transfers()
def _find_auto_batch(self):
+ self.ensure_one()
+ # Check if auto_batch is enabled for this picking.
+ if not self.picking_type_id.auto_batch or self.immediate_transfer or self.batch_id or not self.move_ids or not self._is_auto_batchable():
+ return False
+
# Try to find a compatible batch to insert the picking
possible_batches = self.env['stock.picking.batch'].sudo().search(self._get_possible_batches_domain())
for batch in possible_batches:
@@ -169,6 +186,8 @@ class StockPicking(models.Model):
def _is_auto_batchable(self, picking=None):
""" Verifies if a picking can be put in a batch with another picking without violating auto_batch constrains.
"""
+ if self.state not in ('waiting', 'confirmed', 'assigned'):
+ return False
res = True
if not picking:
picking = self.env['stock.picking']
diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py
index ac63310c7e1..ed13bbac1a2 100644
--- a/addons/stock_picking_batch/models/stock_picking_batch.py
+++ b/addons/stock_picking_batch/models/stock_picking_batch.py
@@ -224,15 +224,19 @@ class StockPickingBatch(models.Model):
self.picking_ids.filtered("show_clear_qty_button").action_clear_quantities_to_zero()
def action_done(self):
+ def has_no_qty_done(picking):
+ return all(float_is_zero(line.qty_done, precision_rounding=line.product_uom_id.rounding) for line in picking.move_line_ids if line.state not in ('done', 'cancel'))
+
self.ensure_one()
self._check_company()
+ # Empty 'waiting for another operation' pickings will be removed from the batch when it is validated.
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
- if any(picking.state not in ('assigned', 'confirmed') for picking in pickings):
- raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.'))
+ empty_waiting_pickings = self.mapped('picking_ids').filtered(lambda p: p.state == 'waiting' and has_no_qty_done(p))
+ pickings = pickings - empty_waiting_pickings
empty_pickings = set()
for picking in pickings:
- if all(float_is_zero(line.qty_done, precision_rounding=line.product_uom_id.rounding) for line in picking.move_line_ids if line.state not in ('done', 'cancel')):
+ if has_no_qty_done(picking):
empty_pickings.add(picking.id)
picking.message_post(
body="%s: %s %s" % (
@@ -241,14 +245,17 @@ class StockPickingBatch(models.Model):
picking.batch_id.id,
picking.batch_id.name))
+ # Run sanity_check as a batch and ignore the one in button_validate() since it is done here.
+ pickings._sanity_check(separate_pickings=False)
+ # Skip sanity_check in pickings button_validate() & remove 'waiting' pickings from the batch
+ context = {'skip_sanity_check': True, 'pickings_to_detach': empty_waiting_pickings.ids}
if len(empty_pickings) == len(pickings):
- return pickings.button_validate()
+ return pickings.with_context(**context).button_validate()
else:
- res = pickings.with_context(skip_immediate=True).button_validate()
- if empty_pickings and res.get('context'):
- res['context']['pickings_to_detach'] = list(empty_pickings)
- return res
-
+ # If some pickings are at least partially done, other pickings (empty & waiting) will be removed from batch without being cancelled in case of no backorder
+ pickings = pickings - self.env['stock.picking'].browse(empty_pickings)
+ context['pickings_to_detach'] = context['pickings_to_detach'] + list(empty_pickings)
+ return pickings.with_context(skip_immediate=True, **context).button_validate()
def action_assign(self):
self.ensure_one()
diff --git a/addons/stock_picking_batch/tests/test_batch_picking.py b/addons/stock_picking_batch/tests/test_batch_picking.py
index a571c016297..a0e597a480d 100644
--- a/addons/stock_picking_batch/tests/test_batch_picking.py
+++ b/addons/stock_picking_batch/tests/test_batch_picking.py
@@ -29,10 +29,12 @@ class TestBatchPicking(TransactionCase):
'categ_id': cls.env.ref('product.product_category_all').id,
})
+ cls.client_1 = cls.env['res.partner'].create({'name': 'Client 1'})
cls.picking_client_1 = cls.env['stock.picking'].create({
'location_id': cls.stock_location.id,
'location_dest_id': cls.customer_location.id,
'picking_type_id': cls.picking_type_out,
+ 'partner_id': cls.client_1.id,
'company_id': cls.env.company.id,
})
@@ -46,10 +48,12 @@ class TestBatchPicking(TransactionCase):
'location_dest_id': cls.customer_location.id,
})
+ cls.client_2 = cls.env['res.partner'].create({'name': 'Client 2'})
cls.picking_client_2 = cls.env['stock.picking'].create({
'location_id': cls.stock_location.id,
'location_dest_id': cls.customer_location.id,
'picking_type_id': cls.picking_type_out,
+ 'partner_id': cls.client_2.id,
'company_id': cls.env.company.id,
})
@@ -315,7 +319,8 @@ class TestBatchPicking(TransactionCase):
self.assertTrue(back_order_wizard_dict)
self.assertEqual(back_order_wizard_dict.get('res_model'), 'stock.backorder.confirmation')
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
- self.assertEqual(len(back_order_wizard.pick_ids), 2)
+ # Empty pickings are excluded from the validation process, to be removed from the batch afterwards.
+ self.assertEqual(len(back_order_wizard.pick_ids), 1)
back_order_wizard.process()
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
diff --git a/addons/stock_picking_batch/wizard/__init__.py b/addons/stock_picking_batch/wizard/__init__.py
index dbf459854ca..6a1895555f3 100644
--- a/addons/stock_picking_batch/wizard/__init__.py
+++ b/addons/stock_picking_batch/wizard/__init__.py
@@ -4,4 +4,3 @@
from . import stock_picking_to_batch
from . import stock_add_to_wave
from . import stock_package_destination
-from . import stock_backorder_confirmation
diff --git a/addons/stock_picking_batch/wizard/stock_backorder_confirmation.py b/addons/stock_picking_batch/wizard/stock_backorder_confirmation.py
deleted file mode 100644
index ba587e08ce6..00000000000
--- a/addons/stock_picking_batch/wizard/stock_backorder_confirmation.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models
-
-
-class StockBackorderConfirmation(models.TransientModel):
- _inherit = 'stock.backorder.confirmation'
-
- def process(self):
- res = super().process()
- if self.env.context.get('pickings_to_detach'):
- self.env['stock.picking'].browse(self.env.context['pickings_to_detach']).batch_id = False
- return res
-
- def process_cancel_backorder(self):
- res = super().process_cancel_backorder()
- if self.env.context.get('pickings_to_detach'):
- self.env['stock.picking'].browse(self.env.context['pickings_to_detach']).batch_id = False
- return res