[FIX] stock_picking_batch_custom: prevent product_id null error on summary lines
- Use regular dict instead of defaultdict to avoid empty entries - Make summary_line_ids readonly=True to prevent UI from inserting empty lines - Add SQL constraint CHECK(product_id IS NOT NULL) as safeguard - Use boolean_toggle widget for is_collected field - Fix tests to use TransactionCase and invalidate_recordset - Add test for empty batch + add pickings + confirm flow
This commit is contained in:
parent
ad8b759643
commit
3eeca66551
5 changed files with 304 additions and 45 deletions
|
|
@ -0,0 +1 @@
|
|||
from . import test_batch_summary # noqa: F401
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestBatchSummary(SavepointCase):
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestBatchSummary(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
|
@ -62,7 +64,7 @@ class TestBatchSummary(SavepointCase):
|
|||
"move_id": move.id,
|
||||
"picking_id": picking.id,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_id": uom.id,
|
||||
"product_uom_id": self.product.uom_id.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"quantity": qty_done,
|
||||
|
|
@ -71,9 +73,45 @@ class TestBatchSummary(SavepointCase):
|
|||
return move
|
||||
|
||||
def _recompute_batch(self):
|
||||
self.batch.invalidate_cache()
|
||||
self.batch.invalidate_recordset()
|
||||
self.batch._compute_summary_line_ids()
|
||||
|
||||
def _create_batch_with_pickings(self):
|
||||
batch = self.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Flow", "picking_type_id": self.picking_type.id}
|
||||
)
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
move = self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 2.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move.line"].create(
|
||||
{
|
||||
"move_id": move.id,
|
||||
"picking_id": picking.id,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_id": self.uom_unit.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
"quantity": 2.0,
|
||||
}
|
||||
)
|
||||
picking.batch_id = batch.id
|
||||
return batch
|
||||
|
||||
# Tests
|
||||
def test_totals_and_pending_with_conversion(self):
|
||||
"""Totals aggregate per product with UoM conversion and pending."""
|
||||
|
|
@ -125,3 +163,127 @@ class TestBatchSummary(SavepointCase):
|
|||
self._recompute_batch()
|
||||
|
||||
self.assertFalse(self.batch.summary_line_ids)
|
||||
|
||||
def test_no_required_product_error_on_confirm(self):
|
||||
"""Confirming batch with pickings must not create summary lines without product."""
|
||||
|
||||
batch = self._create_batch_with_pickings()
|
||||
|
||||
# Trigger compute via confirm flow
|
||||
batch.action_confirm()
|
||||
|
||||
self.assertTrue(batch.summary_line_ids)
|
||||
self.assertFalse(
|
||||
batch.summary_line_ids.filtered(lambda line: not line.product_id)
|
||||
)
|
||||
|
||||
def test_empty_batch_add_pickings_then_confirm(self):
|
||||
"""Create empty draft batch, add multiple pickings, then confirm."""
|
||||
|
||||
# 1. Create empty batch in draft state
|
||||
batch = self.env["stock.picking.batch"].create(
|
||||
{"name": "Batch Empty Start", "picking_type_id": self.picking_type.id}
|
||||
)
|
||||
self.assertEqual(batch.state, "draft")
|
||||
self.assertFalse(batch.picking_ids)
|
||||
self.assertFalse(batch.summary_line_ids)
|
||||
|
||||
# 2. Create pickings with moves (without batch assignment)
|
||||
product2 = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product 2",
|
||||
"uom_id": self.uom_unit.id,
|
||||
"uom_po_id": self.uom_unit.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_a = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_a.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_b = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": product2.name,
|
||||
"product_id": product2.id,
|
||||
"product_uom_qty": 10.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_b.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_c = self.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": self.picking_type.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
self.env["stock.move"].create(
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 3.0,
|
||||
"product_uom": self.uom_unit.id,
|
||||
"picking_id": picking_c.id,
|
||||
"location_id": self.location_src.id,
|
||||
"location_dest_id": self.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Add pickings to the batch
|
||||
picking_a.batch_id = batch
|
||||
picking_b.batch_id = batch
|
||||
picking_c.batch_id = batch
|
||||
|
||||
self.assertEqual(len(batch.picking_ids), 3)
|
||||
|
||||
# 4. Confirm the batch — this should not raise product_id required error
|
||||
batch.action_confirm()
|
||||
|
||||
self.assertEqual(batch.state, "in_progress")
|
||||
|
||||
# 5. Verify summary lines are correct
|
||||
self.assertTrue(batch.summary_line_ids)
|
||||
self.assertFalse(
|
||||
batch.summary_line_ids.filtered(lambda line: not line.product_id)
|
||||
)
|
||||
|
||||
# Two products expected
|
||||
products_in_summary = batch.summary_line_ids.mapped("product_id")
|
||||
self.assertIn(self.product, products_in_summary)
|
||||
self.assertIn(product2, products_in_summary)
|
||||
|
||||
# Check aggregated quantities
|
||||
line_product1 = batch.summary_line_ids.filtered(
|
||||
lambda line: line.product_id == self.product
|
||||
)
|
||||
self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3
|
||||
|
||||
line_product2 = batch.summary_line_ids.filtered(
|
||||
lambda line: line.product_id == product2
|
||||
)
|
||||
self.assertAlmostEqual(line_product2.qty_demanded, 10.0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue