# Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import Command from odoo.tests import Form from odoo.tests import TransactionCase from odoo.tests import tagged @tagged("-at_install", "post_install") class TestManualQuantity(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.uom_kg = cls.env.ref("uom.product_uom_kgm") cls.stock_location = cls.env.ref("stock.stock_location_stock") cls.customer_location = cls.env.ref("stock.stock_location_customers") cls.picking_type_out = cls.env.ref("stock.picking_type_out") cls.apples = cls.env["product.product"].create( { "name": "Apples", "is_storable": True, "uom_id": cls.uom_kg.id, "uom_po_id": cls.uom_kg.id, } ) cls.bread = cls.env["product.product"].create( { "name": "Bread", "is_storable": True, "uom_id": cls.uom_kg.id, "uom_po_id": cls.uom_kg.id, } ) for product in cls.apples | cls.bread: cls.env["stock.quant"]._update_available_quantity( product, cls.stock_location, 100.0 ) # Helpers def _create_picking(self, products_qty): """Create a confirmed and reserved delivery for ``{product: demand}``.""" picking = self.env["stock.picking"].create( { "picking_type_id": self.picking_type_out.id, "location_id": self.stock_location.id, "location_dest_id": self.customer_location.id, "move_ids": [ Command.create( { "name": product.name, "product_id": product.id, "product_uom": product.uom_id.id, "product_uom_qty": qty, "location_id": self.stock_location.id, "location_dest_id": self.customer_location.id, } ) for product, qty in products_qty.items() ], } ) picking.action_confirm() picking.action_assign() return picking def _type_quantity(self, line, quantity): """Reproduce what the web client sends when a user types a quantity. The onchange marks the line as picked, so both fields travel together in the same write. """ edited = line.new(origin=line) edited.quantity = quantity edited._onchange_quantity_manual() line.write({"quantity": quantity, "picked": edited.picked}) # Tests def test_onchange_marks_the_line_as_picked(self): picking = self._create_picking({self.apples: 1.0}) line = picking.move_ids.move_line_ids edited = line.new(origin=line) edited.quantity = 0.87 edited._onchange_quantity_manual() self.assertTrue(edited.picked) def test_reservation_does_not_restore_a_lowered_quantity(self): picking = self._create_picking({self.apples: 1.0}) move = picking.move_ids self.assertEqual(move.quantity, 1.0) self._type_quantity(move.move_line_ids, 0.87) self.assertTrue(move.picked) self.assertEqual(move.product_uom_qty, 0.87) self.assertEqual(move.state, "assigned") # The two entry points that used to top the move back up to its demand. move._action_assign() self.env["procurement.group"].run_scheduler() self.assertEqual(move.quantity, 0.87) self.assertEqual(move.move_line_ids.quantity, 0.87) def test_a_raised_quantity_keeps_its_demand(self): picking = self._create_picking({self.apples: 1.0}) move = picking.move_ids self._type_quantity(move.move_line_ids, 1.2) self.assertEqual(move.product_uom_qty, 1.0) self.assertEqual(move.state, "assigned") move._action_assign() self.assertEqual(move.quantity, 1.2) def test_an_empty_line_keeps_its_demand(self): """A line left at zero still offers the standard backorder choice.""" picking = self._create_picking({self.apples: 1.0}) move = picking.move_ids self._type_quantity(move.move_line_ids, 0.0) self.assertEqual(move.product_uom_qty, 1.0) def test_reserving_does_not_mark_moves_as_picked(self): """Only a person marks a move as picked, never the reservation.""" picking = self._create_picking({self.apples: 1.0}) picking.move_ids._action_assign() self.env["procurement.group"].run_scheduler() self.assertFalse(picking.move_ids.picked) self.assertFalse(picking.move_ids.move_line_ids.picked) def test_editing_the_move_quantity_freezes_it_too(self): """The Operations tab of a transfer edits the quantity on the move.""" picking = self._create_picking({self.apples: 1.0}) move = picking.move_ids with Form(picking) as picking_form: with picking_form.move_ids_without_package.edit(0) as move_form: move_form.quantity = 0.87 self.assertTrue(move.picked) self.assertEqual(move.product_uom_qty, 0.87) move._action_assign() self.assertEqual(move.quantity, 0.87) def test_untouched_lines_are_still_validated(self): """Freezing one line must not push the others to a backorder.""" picking = self._create_picking({self.apples: 1.0, self.bread: 2.0}) apples_move = picking.move_ids.filtered( lambda move: move.product_id == self.apples ) bread_move = picking.move_ids - apples_move self._type_quantity(apples_move.move_line_ids, 0.87) picking.button_validate() self.assertEqual(picking.state, "done") self.assertEqual(apples_move.quantity, 0.87) self.assertEqual(bread_move.state, "done") self.assertEqual(bread_move.quantity, 2.0) self.assertFalse(picking.backorder_ids) def test_operation_views_load_the_picked_field(self): """The client only sends back the fields present in the arch.""" move_line = self.env["stock.move.line"] for xml_id in ( "stock.view_stock_move_line_operation_tree", "stock.view_stock_move_line_detailed_operation_tree", ): with self.subTest(view=xml_id): view = move_line.get_view(self.env.ref(xml_id).id, "list") self.assertIn('name="picked"', view["arch"])