[IMP] stock: picking sanity_check

Move the sanity check from the `button_validate()` outside so it can be
run premptively in a different context.

Basically it allows batches to run the santiy_check as a whole. This
means that when the function checks if there is at least one quantity
done to check only those move_lines for the lot/serial numbers, it is
now able to check the pickings independantly from each others like
before, but also as if they were a single entity.

This can avoid this weird behavior in the following batch :

Picking | product_name   | qty | done_qty
--------|----------------|-----|---------
Pick_1  | product_1      |   2 |       0
Pick_1  | product_serial |   1 |       0
Pick_2  | product_2      |   2 |       0

Setting a done_qty on product_1 would be fine to validate the batch,
since at least one done_qty would be found within Pick_1. But setting a
done_qty on product_2 would raise an error for missing serial numbers
since then no done_qty would be found within Pick_1.

This commit evens the behavior of the sanitizer in this case. While
keeping the way it works outside of a batch validation.

Task-2971346

X-original-commit: 3a3b5d90d58339f65b1db0b52a2c81e02b5a3f17
Part-of: odoo/odoo#102382
This commit is contained in:
clesgow
2022-10-06 18:00:48 +02:00
parent 1b2b759a49
commit d7488f56f4
+64 -32
View File
@@ -965,44 +965,59 @@ class Picking(models.Model):
for pl in package_level_ids:
pl.location_dest_id = self._get_entire_pack_location_dest(pl.move_line_ids) or picking.location_dest_id.id
def do_unreserve(self):
self.move_ids._do_unreserve()
self.package_level_ids.filtered(lambda p: not p.move_ids).unlink()
def _get_lot_move_lines_for_sanity_check(self, none_done_picking_ids, separate_pickings=True):
""" Get all move_lines with tracked products that need to be checked over in the sanity check.
:param none_done_picking_ids: Set of all pickings ids that have no qty_done set on any move_line.
:param separate_pickings: Indicates if pickings should be checked independently for lot/serial numbers or not.
"""
def get_relevant_move_line_ids(none_done_picking_ids, picking):
# Get all move_lines if picking has no qty_done set, otherwise only get the move_lines with some qty_done set.
if picking.id in none_done_picking_ids:
return picking.move_line_ids.filtered(lambda ml: ml.product_id and ml.product_id.tracking != 'none').ids
else:
return get_line_with_done_qty_ids(picking.move_line_ids)
def button_validate(self):
# Clean-up the context key at validation to avoid forcing the creation of immediate
# transfers.
ctx = dict(self.env.context)
ctx.pop('default_immediate_transfer', None)
self = self.with_context(ctx)
def get_line_with_done_qty_ids(move_lines):
# Get only move_lines that has some qty_done set.
return move_lines.filtered(lambda ml: ml.product_id and ml.product_id.tracking != 'none' and float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)).ids
# Sanity checks.
pickings_without_moves = self.browse()
pickings_without_quantities = self.browse()
if separate_pickings:
# If pickings are checked independently, get full/partial move_lines depending if each picking has no qty_done set.
lines_to_check_ids = [line_id for picking in self for line_id in get_relevant_move_line_ids(none_done_picking_ids, picking)]
else:
# If pickings are checked as one (like in a batch), then get only the move_lines with qty_done across all pickings if there is at least one.
if any(picking.id not in none_done_picking_ids for picking in self):
lines_to_check_ids = get_line_with_done_qty_ids(self.move_line_ids)
else:
lines_to_check_ids = self.move_line_ids.filtered(lambda ml: ml.product_id and ml.product_id.tracking != 'none').ids
return self.env['stock.move.line'].browse(lines_to_check_ids)
def _sanity_check(self, separate_pickings=True):
""" Sanity check for `button_validate()`
:param separate_pickings: Indicates if pickings should be checked independently for lot/serial numbers or not.
"""
pickings_without_lots = self.browse()
products_without_lots = self.env['product.product']
pickings_without_moves = self.filtered(lambda p: not p.move_ids and not p.move_line_ids)
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
no_quantities_done_ids = set()
no_reserved_quantities_ids = set()
for picking in self:
if not picking.move_ids and not picking.move_line_ids:
pickings_without_moves |= picking
if all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in picking.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))):
no_quantities_done_ids.add(picking.id)
if all(float_is_zero(move_line.reserved_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in picking.move_line_ids):
no_reserved_quantities_ids.add(picking.id)
pickings_without_quantities = self.filtered(lambda p: p.id in no_quantities_done_ids and p.id in no_reserved_quantities_ids)
picking.message_subscribe([self.env.user.partner_id.id])
picking_type = picking.picking_type_id
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in picking.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel')))
no_reserved_quantities = all(float_is_zero(move_line.reserved_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in picking.move_line_ids)
if no_reserved_quantities and no_quantities_done:
pickings_without_quantities |= picking
if picking_type.use_create_lots or picking_type.use_existing_lots:
lines_to_check = picking.move_line_ids
if not no_quantities_done:
lines_to_check = lines_to_check.filtered(lambda line: float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding))
for line in lines_to_check:
product = line.product_id
if product and product.tracking != 'none':
if not line.lot_name and not line.lot_id:
pickings_without_lots |= picking
products_without_lots |= product
pickings_using_lots = self.filtered(lambda p: p.picking_type_id.use_create_lots or p.picking_type_id.use_existing_lots)
if pickings_using_lots:
lines_to_check = pickings_using_lots._get_lot_move_lines_for_sanity_check(no_quantities_done_ids, separate_pickings)
for line in lines_to_check:
if not line.lot_name and not line.lot_id:
pickings_without_lots |= line.picking_id
products_without_lots |= line.product_id
if not self._should_show_transfers():
if pickings_without_moves:
@@ -1022,6 +1037,23 @@ class Picking(models.Model):
if message:
raise UserError(message.lstrip())
def do_unreserve(self):
self.move_ids._do_unreserve()
self.package_level_ids.filtered(lambda p: not p.move_ids).unlink()
def button_validate(self):
# Clean-up the context key at validation to avoid forcing the creation of immediate
# transfers.
ctx = dict(self.env.context)
ctx.pop('default_immediate_transfer', None)
self = self.with_context(ctx)
# Sanity checks.
if not self.env.context.get('skip_sanity_check', False):
self._sanity_check()
self.message_subscribe([self.env.user.partner_id.id])
# Run the pre-validation wizards. Processing a pre-validation wizard should work on the
# moves and/or the context and never call `_action_done`.
if not self.env.context.get('button_validate_picking_ids'):