[FIX] stock_picking_batch_custom: validar collected solo en operaciones detalladas

This commit is contained in:
snt 2026-04-08 18:29:55 +02:00
parent 2237cba034
commit 05a8908007
4 changed files with 63 additions and 27 deletions

View file

@ -12,7 +12,7 @@ class StockBackorderConfirmation(models.TransientModel):
self.env.context.get("button_validate_picking_ids", [])
)
for batch in pickings.batch_id:
batch._check_all_products_collected()
batch._check_all_products_collected(pickings)
def process(self):
self._check_batch_summary_lines()

View file

@ -43,15 +43,13 @@ class StockPicking(models.Model):
batch_pickings = self.filtered(
lambda picking, batch=batch: picking.batch_id == batch
)
processed_product_ids = (
batch_pickings.move_line_ids.filtered(
lambda line: line.move_id.state not in ("cancel", "done")
processed_move_lines = batch_pickings.move_line_ids.filtered(
lambda line: (
line.move_id.state not in ("cancel", "done")
and line.product_id
and line.quantity
and line.move_id.quantity > 0
)
.mapped("product_id")
.ids
)
if not processed_product_ids:
if not processed_move_lines:
continue
batch._check_all_products_collected(processed_product_ids)
batch._check_all_products_collected(batch_pickings)

View file

@ -140,29 +140,39 @@ class StockPickingBatch(models.Model):
else:
batch.summary_line_ids = [fields.Command.clear()]
def _check_all_products_collected(self, product_ids=None):
"""Ensure collected is checked for processed products only.
def _check_all_products_collected(self, pickings=None):
"""Validate only Detailed Operations collected flags.
The product summary remains informative until Odoo knows which products
are actually being validated after the backorder decision.
Product Summary checkboxes are informative and must not block the flow.
The blocking validation applies to stock.move.line.is_collected in the
detailed operations tab for lines with processed quantity.
"""
for batch in self:
not_collected_lines = batch.summary_line_ids.filtered(
batch_pickings = (
pickings.filtered(lambda p, batch=batch: p.batch_id == batch)
if pickings
else batch.picking_ids
)
if not batch_pickings:
continue
not_collected_move_lines = batch_pickings.move_line_ids.filtered(
lambda line: (
line.qty_done > 0
line.move_id.state not in ("cancel", "done")
and line.product_id
and line.move_id.quantity > 0
and not line.is_collected
and (not product_ids or line.product_id.id in product_ids)
)
)
if not not_collected_lines:
if not not_collected_move_lines:
continue
product_names = ", ".join(
not_collected_lines.mapped("product_id.display_name")
sorted(set(not_collected_move_lines.mapped("product_id.display_name")))
)
message = batch.env._(
"You must mark all product lines as collected before validating the batch."
"You must mark detailed operation lines as collected before validating the batch."
)
if product_names:
message += "\n" + batch.env._(

View file

@ -344,19 +344,38 @@ class TestBatchSummary(TransactionCase):
)
self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
def test_done_requires_all_summary_lines_collected_without_backorder(self):
"""Full validation still blocks if processed products are unchecked."""
def test_done_requires_detailed_lines_collected_without_backorder(self):
"""Full validation blocks when detailed operation lines are unchecked."""
batch = self._create_batch_with_pickings()
batch.action_confirm()
batch._compute_summary_line_ids()
self.assertTrue(batch.summary_line_ids)
self.assertFalse(batch.summary_line_ids.mapped("is_collected")[0])
detail_lines = batch.move_line_ids.filtered(lambda line: line.quantity > 0)
self.assertTrue(detail_lines)
self.assertFalse(any(detail_lines.mapped("is_collected")))
with self.assertRaises(UserError):
batch.action_done()
def test_done_ignores_product_summary_checkbox(self):
"""Product Summary checkbox is informative and must not block validation."""
batch = self._create_batch_with_pickings()
batch.action_confirm()
batch._compute_summary_line_ids()
# Keep Product Summary unchecked on purpose
self.assertFalse(any(batch.summary_line_ids.mapped("is_collected")))
# Detailed lines drive validation
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
{"is_collected": True}
)
# Should not raise
batch.action_done()
def test_partial_validation_waits_for_backorder_wizard_before_blocking(self):
"""Partial batch validation must open the backorder wizard first."""
@ -373,13 +392,20 @@ class TestBatchSummary(TransactionCase):
self.assertIsInstance(action, dict)
self.assertEqual(action.get("res_model"), "stock.backorder.confirmation")
def test_partial_validation_checks_only_processed_products(self):
"""Unchecked lines with no processed quantity must remain informative."""
def test_partial_validation_checks_only_processed_detailed_lines(self):
"""Only processed detailed lines must be required for collected."""
batch = self._create_partial_batch(extra_move=True)
# Ensure product_2 remains non-processed in this scenario
move_product_2 = batch.move_ids.filtered(
lambda move: move.product_id == self.product_2
)
move_product_2.write({"quantity": 0.0})
move_product_2.move_line_ids.write({"quantity": 0.0})
with self.assertRaisesRegex(UserError, self.product.display_name) as err:
batch._check_all_products_collected([self.product.id])
batch._check_all_products_collected(batch.picking_ids)
self.assertNotIn(self.product_2.display_name, str(err.exception))
@ -389,7 +415,9 @@ class TestBatchSummary(TransactionCase):
batch = self._create_batch_with_pickings()
batch.action_confirm()
batch._compute_summary_line_ids()
batch.summary_line_ids.write({"is_collected": True})
batch.move_line_ids.filtered(lambda line: line.quantity > 0).write(
{"is_collected": True}
)
# Should not raise
batch._check_all_products_collected()