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