[FIX] stock_picking_batch: Backorder autobatch

When a backorder was created and the auto_batch was activated for this
picking_type, no batch was assigned to the created backorder, even if it
matched the auto-batch criterias.

Now when a picking is done, we check if backorders were created. If they
are eligible for auto-batchs (given their picking_type, etc.), they also
get the auto_batch done for them.

We can't simply use the previous picking's batch, because if this batch
is now done, then introducing a non-done picking into the batch would
raise an inconsistency between the pickings' states and raise an error.

Removes the old override of backorder confirmation as now batch
dissociation is done in the picking's 'action_done()' override.

Allows the validation of a batch that has pickings 'waiting for another
operation', as they now will get removed from the batch in the process,
no longer blocking the batch's validation.

Finally, makes that pickings that had no quantities done when validating
a batch aren't cancelled when selecting 'No backorder' in the
confirmation wizard, but are instead removed from the batch.

Task-2971346

X-original-commit: 1720840b7bd9b3e3e90f7cf860f60f1d0746ce63
Part-of: odoo/odoo#102382
This commit is contained in:
clesgow
2022-10-06 18:00:48 +02:00
parent d7488f56f4
commit cd2dbf0a67
7 changed files with 54 additions and 43 deletions
@@ -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"
@@ -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
@@ -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']
@@ -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="<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>" % (
@@ -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()
@@ -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')
@@ -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
@@ -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