# Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.tests.common import SavepointCase class TestBatchSummary(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() cls.uom_unit = cls.env.ref("uom.product_uom_unit") cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") cls.location_src = cls.env.ref("stock.stock_location_stock") cls.location_dest = cls.env.ref("stock.stock_location_customers") cls.picking_type = cls.env.ref("stock.picking_type_out") cls.product = cls.env["product.product"].create( { "name": "Test Product", "uom_id": cls.uom_unit.id, "uom_po_id": cls.uom_unit.id, } ) cls.batch = cls.env["stock.picking.batch"].create( {"name": "Batch Test", "picking_type_id": cls.picking_type.id} ) cls.picking1 = cls.env["stock.picking"].create( { "picking_type_id": cls.picking_type.id, "location_id": cls.location_src.id, "location_dest_id": cls.location_dest.id, "batch_id": cls.batch.id, } ) cls.picking2 = cls.env["stock.picking"].create( { "picking_type_id": cls.picking_type.id, "location_id": cls.location_src.id, "location_dest_id": cls.location_dest.id, "batch_id": cls.batch.id, } ) # Helpers def _add_move(self, picking, qty_demanded, qty_done, uom): move = self.env["stock.move"].create( { "name": self.product.name, "product_id": self.product.id, "product_uom_qty": qty_demanded, "product_uom": uom.id, "picking_id": picking.id, "location_id": self.location_src.id, "location_dest_id": self.location_dest.id, } ) if qty_done: self.env["stock.move.line"].create( { "move_id": move.id, "picking_id": picking.id, "product_id": self.product.id, "product_uom_id": uom.id, "location_id": self.location_src.id, "location_dest_id": self.location_dest.id, "quantity": qty_done, } ) return move def _recompute_batch(self): self.batch.invalidate_cache() self.batch._compute_summary_line_ids() # Tests def test_totals_and_pending_with_conversion(self): """Totals aggregate per product with UoM conversion and pending.""" # demand 12 units, done 5 units self._add_move(self.picking1, qty_demanded=12, qty_done=5, uom=self.uom_unit) # demand 2 dozens (24 units), done 6 units self._add_move(self.picking2, qty_demanded=2, qty_done=6, uom=self.uom_dozen) self._recompute_batch() self.assertEqual(len(self.batch.summary_line_ids), 1) line = self.batch.summary_line_ids self.assertEqual(line.product_id, self.product) self.assertEqual(line.product_uom_id, self.uom_unit) self.assertAlmostEqual(line.qty_demanded, 36.0) self.assertAlmostEqual(line.qty_done, 11.0) self.assertAlmostEqual(line.qty_pending, 25.0) def test_collected_flag_preserved_on_recompute(self): """Collected stays checked after totals change.""" self._add_move(self.picking1, qty_demanded=1, qty_done=1, uom=self.uom_unit) self._recompute_batch() line = self.batch.summary_line_ids line.is_collected = True # Add more demand/done to trigger an update self._add_move(self.picking1, qty_demanded=3, qty_done=2, uom=self.uom_unit) self._recompute_batch() self.assertTrue(self.batch.summary_line_ids.is_collected) def test_cancelled_moves_are_ignored(self): """Cancelled moves do not count in the summary and lines are removed.""" move1 = self._add_move( self.picking1, qty_demanded=4, qty_done=2, uom=self.uom_unit ) move2 = self._add_move( self.picking2, qty_demanded=6, qty_done=3, uom=self.uom_unit ) self._recompute_batch() self.assertEqual(len(self.batch.summary_line_ids), 1) move1.write({"state": "cancel"}) move2.write({"state": "cancel"}) self._recompute_batch() self.assertFalse(self.batch.summary_line_ids)