[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", []) self.env.context.get("button_validate_picking_ids", [])
) )
for batch in pickings.batch_id: for batch in pickings.batch_id:
batch._check_all_products_collected() batch._check_all_products_collected(pickings)
def process(self): def process(self):
self._check_batch_summary_lines() self._check_batch_summary_lines()

View file

@ -43,15 +43,13 @@ class StockPicking(models.Model):
batch_pickings = self.filtered( batch_pickings = self.filtered(
lambda picking, batch=batch: picking.batch_id == batch lambda picking, batch=batch: picking.batch_id == batch
) )
processed_product_ids = ( processed_move_lines = batch_pickings.move_line_ids.filtered(
batch_pickings.move_line_ids.filtered( lambda line: (
lambda line: line.move_id.state not in ("cancel", "done") line.move_id.state not in ("cancel", "done")
and line.product_id 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 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: else:
batch.summary_line_ids = [fields.Command.clear()] batch.summary_line_ids = [fields.Command.clear()]
def _check_all_products_collected(self, product_ids=None): def _check_all_products_collected(self, pickings=None):
"""Ensure collected is checked for processed products only. """Validate only Detailed Operations collected flags.
The product summary remains informative until Odoo knows which products Product Summary checkboxes are informative and must not block the flow.
are actually being validated after the backorder decision. The blocking validation applies to stock.move.line.is_collected in the
detailed operations tab for lines with processed quantity.
""" """
for batch in self: 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: ( 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 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 continue
product_names = ", ".join( 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._( 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: if product_names:
message += "\n" + batch.env._( message += "\n" + batch.env._(

View file

@ -344,19 +344,38 @@ class TestBatchSummary(TransactionCase):
) )
self.assertAlmostEqual(line_product2.qty_demanded, 10.0) self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
def test_done_requires_all_summary_lines_collected_without_backorder(self): def test_done_requires_detailed_lines_collected_without_backorder(self):
"""Full validation still blocks if processed products are unchecked.""" """Full validation blocks when detailed operation lines are unchecked."""
batch = self._create_batch_with_pickings() batch = self._create_batch_with_pickings()
batch.action_confirm() batch.action_confirm()
batch._compute_summary_line_ids() batch._compute_summary_line_ids()
self.assertTrue(batch.summary_line_ids) detail_lines = batch.move_line_ids.filtered(lambda line: line.quantity > 0)
self.assertFalse(batch.summary_line_ids.mapped("is_collected")[0]) self.assertTrue(detail_lines)
self.assertFalse(any(detail_lines.mapped("is_collected")))
with self.assertRaises(UserError): with self.assertRaises(UserError):
batch.action_done() 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): def test_partial_validation_waits_for_backorder_wizard_before_blocking(self):
"""Partial batch validation must open the backorder wizard first.""" """Partial batch validation must open the backorder wizard first."""
@ -373,13 +392,20 @@ class TestBatchSummary(TransactionCase):
self.assertIsInstance(action, dict) self.assertIsInstance(action, dict)
self.assertEqual(action.get("res_model"), "stock.backorder.confirmation") self.assertEqual(action.get("res_model"), "stock.backorder.confirmation")
def test_partial_validation_checks_only_processed_products(self): def test_partial_validation_checks_only_processed_detailed_lines(self):
"""Unchecked lines with no processed quantity must remain informative.""" """Only processed detailed lines must be required for collected."""
batch = self._create_partial_batch(extra_move=True) 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: 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)) self.assertNotIn(self.product_2.display_name, str(err.exception))
@ -389,7 +415,9 @@ class TestBatchSummary(TransactionCase):
batch = self._create_batch_with_pickings() batch = self._create_batch_with_pickings()
batch.action_confirm() batch.action_confirm()
batch._compute_summary_line_ids() 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 # Should not raise
batch._check_all_products_collected() batch._check_all_products_collected()