[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:
@@ -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
|
||||
Reference in New Issue
Block a user