diff --git a/shopfloor/models/stock_move.py b/shopfloor/models/stock_move.py index acf7371576..da24e18d02 100644 --- a/shopfloor/models/stock_move.py +++ b/shopfloor/models/stock_move.py @@ -1,4 +1,4 @@ -from odoo import models +from odoo import _, models class StockMove(models.Model): @@ -36,3 +36,45 @@ def _action_done(self, cancel_backorder=False): if picking.state == "done": picking.action_done() return moves + + def extract_and_action_done(self): + """Extract the moves in a separate transfer and validate them. + + You can combine this method with `split_other_move_lines` method + to first extract some move lines in a separate move, then validate it + with this method. + """ + moves = self.filtered(lambda m: m.state == "assigned") + if not moves: + return False + for picking in moves.picking_id: + moves_todo = picking.move_lines & moves + if moves_todo == picking.move_lines: + # No need to create a new transfer if we are processing all moves + new_picking = picking + else: + new_picking = picking.copy( + { + "name": "/", + "move_lines": [], + "move_line_ids": [], + "backorder_id": picking.id, + } + ) + new_picking.message_post( + body=_( + "Created from backorder " + "%s." + ) + % (picking.id, picking.name) + ) + moves_todo.write({"picking_id": new_picking.id}) + moves_todo.package_level_id.write({"picking_id": new_picking.id}) + moves_todo.move_line_ids.write({"picking_id": new_picking.id}) + moves_todo.move_line_ids.package_level_id.write( + {"picking_id": new_picking.id} + ) + new_picking.action_assign() + assert new_picking.state == "assigned" + new_picking.action_done() + return True diff --git a/shopfloor/tests/__init__.py b/shopfloor/tests/__init__.py index bfa6c153a4..ca80513434 100644 --- a/shopfloor/tests/__init__.py +++ b/shopfloor/tests/__init__.py @@ -51,3 +51,4 @@ from . import test_zone_picking_unload_all from . import test_zone_picking_unload_set_destination from . import test_misc +from . import test_stock_split diff --git a/shopfloor/tests/test_stock_split.py b/shopfloor/tests/test_stock_split.py index 9a3ac8e852..b39e22878f 100644 --- a/shopfloor/tests/test_stock_split.py +++ b/shopfloor/tests/test_stock_split.py @@ -14,7 +14,7 @@ def setUpClass(cls): cls.pack_location = cls.warehouse.wh_pack_stock_loc_id cls.ship_location = cls.warehouse.wh_output_stock_loc_id cls.stock_location = cls.env.ref("stock.stock_location_stock") - # Create a product + # Create products cls.product_a = ( cls.env["product.product"] .sudo() @@ -39,6 +39,30 @@ def setUpClass(cls): } ) ) + cls.product_b = ( + cls.env["product.product"] + .sudo() + .create( + { + "name": "Product B", + "type": "product", + "default_code": "B", + "barcode": "B", + "weight": 2, + } + ) + ) + cls.product_b_packaging = ( + cls.env["product.packaging"] + .sudo() + .create( + { + "name": "Box", + "product_id": cls.product_b.id, + "barcode": "ProductBBox", + } + ) + ) # Put product_a quantities in different packages to get several move lines cls.package_1 = cls.env["stock.quant.package"].create({"name": "PACKAGE_1"}) cls.package_2 = cls.env["stock.quant.package"].create({"name": "PACKAGE_2"}) @@ -53,6 +77,8 @@ def setUpClass(cls): cls._update_qty_in_location( cls.stock_location, cls.product_a, 5, package=cls.package_3 ) + # Put product_b quantities in stock + cls._update_qty_in_location(cls.stock_location, cls.product_b, 10) # Create the pick/pack/ship transfer cls.ship_move_a = cls.env["stock.move"].create( { @@ -68,12 +94,28 @@ def setUpClass(cls): "state": "draft", } ) - cls.ship_move_a._assign_picking() - cls.ship_move_a._action_confirm(merge=False) - cls.pack_move = cls.ship_move_a.move_orig_ids[0] - cls.pick_move = cls.pack_move.move_orig_ids[0] - cls.picking = cls.pick_move.picking_id - cls.packing = cls.pack_move.picking_id + cls.ship_move_b = cls.env["stock.move"].create( + { + "name": cls.product_b.display_name, + "product_id": cls.product_b.id, + "product_uom_qty": 4, + "product_uom": cls.product_b.uom_id.id, + "location_id": cls.ship_location.id, + "location_dest_id": cls.customer_location.id, + "warehouse_id": cls.warehouse.id, + "picking_type_id": cls.warehouse.out_type_id.id, + "procure_method": "make_to_order", + "state": "draft", + } + ) + (cls.ship_move_a | cls.ship_move_b)._assign_picking() + (cls.ship_move_a | cls.ship_move_b)._action_confirm(merge=False) + cls.pack_move_a = cls.ship_move_a.move_orig_ids[0] + cls.pick_move_a = cls.pack_move_a.move_orig_ids[0] + cls.pack_move_b = cls.ship_move_b.move_orig_ids[0] + cls.pick_move_b = cls.pack_move_b.move_orig_ids[0] + cls.picking = cls.pick_move_a.picking_id + cls.packing = cls.pack_move_a.picking_id cls.picking.action_assign() @classmethod @@ -90,27 +132,27 @@ def _update_qty_in_location( ) def test_split_pickings_from_source_location(self): - dest_location = self.pick_move.location_dest_id.sudo().copy( + dest_location = self.pick_move_a.location_dest_id.sudo().copy( { - "name": self.pick_move.location_dest_id.name + "_2", - "barcode": self.pick_move.location_dest_id.barcode + "_2", - "location_id": self.pick_move.location_dest_id.id, + "name": self.pick_move_a.location_dest_id.name + "_2", + "barcode": self.pick_move_a.location_dest_id.barcode + "_2", + "location_id": self.pick_move_a.location_dest_id.id, } ) # Pick goods from stock and move some of them to a different destination - self.assertEqual(self.pick_move.state, "assigned") - for i, move_line in enumerate(self.pick_move.move_line_ids): + self.assertEqual(self.pick_move_a.state, "assigned") + for i, move_line in enumerate(self.pick_move_a.move_line_ids): move_line.qty_done = move_line.product_uom_qty if i % 2: move_line.location_dest_id = dest_location - self.pick_move.with_context(_sf_no_backorder=True)._action_done() - self.assertEqual(self.pick_move.state, "done") + self.pick_move_a.with_context(_sf_no_backorder=True)._action_done() + self.assertEqual(self.pick_move_a.state, "done") # Pack step, we want to split move lines from common source location - self.assertEqual(self.pack_move.state, "assigned") - move_lines_to_process = self.pack_move.move_line_ids.filtered( + self.assertEqual(self.pack_move_a.state, "assigned") + move_lines_to_process = self.pack_move_a.move_line_ids.filtered( lambda ml: ml.location_id == dest_location ) - self.assertEqual(len(self.pack_move.move_line_ids), 3) + self.assertEqual(len(self.pack_move_a.move_line_ids), 3) self.assertEqual(len(self.packing.package_level_ids), 3) self.assertEqual(len(move_lines_to_process), 1) new_packing = move_lines_to_process._split_pickings_from_source_location() @@ -120,9 +162,39 @@ def test_split_pickings_from_source_location(self): self.assertTrue(new_packing != self.packing) self.assertEqual(new_packing.backorder_id, self.packing) self.assertEqual( - self.pick_move.move_dest_ids.picking_id, self.packing | new_packing + self.pick_move_a.move_dest_ids.picking_id, self.packing | new_packing ) self.assertEqual(move_lines_to_process.state, "assigned") self.assertEqual( - set(self.pack_move.move_line_ids.mapped("state")), {"assigned"} + set(self.pack_move_a.move_line_ids.mapped("state")), {"assigned"} ) + + def test_extract_and_action_done_one_move(self): + self.assertFalse(self.picking.backorder_ids) + self.assertEqual(self.picking.state, "assigned") + for move_line in self.pick_move_b.move_line_ids: + move_line.qty_done = move_line.product_uom_qty + self.pick_move_b.extract_and_action_done() + new_picking = self.picking.backorder_ids + self.assertTrue(new_picking) + # Check move lines repartition + self.assertNotIn(self.pick_move_b, self.picking.move_lines) + self.assertEqual(new_picking.move_lines, self.pick_move_b) + # Check states + self.assertEqual(self.picking.state, "assigned") + self.assertEqual(self.pick_move_b.state, "done") + self.assertEqual(new_picking.state, "done") + + def test_extract_and_action_done_several_moves(self): + self.assertFalse(self.picking.backorder_ids) + self.assertEqual(self.picking.state, "assigned") + for move_line in self.picking.move_line_ids: + move_line.qty_done = move_line.product_uom_qty + self.picking.move_lines.extract_and_action_done() + # No backorder as all moves of the picking have been validated + new_picking = self.picking.backorder_ids + self.assertFalse(new_picking) + # Check move lines repartition + self.assertEqual(len(self.picking.move_lines), 2) + # Check states + self.assertEqual(self.picking.state, "done")