From dbec76ef344667e4d1de0e1957c5644d5b49b1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Thu, 17 Sep 2020 13:13:08 +0200 Subject: [PATCH] new method to validate moves in a separate transfer (#72) Calling _action_done() directly on the stock.move while leaving the stock.picking "assigned" with the other moves already required various workarounds as it is normally not an expected flow in Odoo. This method is to be used in services instead of calling _action_done() on the moves. If a move to set to done contains move lines that should not be validated, the line to keep should be extracted beforehand with `move_id.split_other_move_lines`. --- shopfloor/models/stock_move.py | 44 ++++++++++- shopfloor/tests/__init__.py | 1 + shopfloor/tests/test_stock_split.py | 112 +++++++++++++++++++++++----- 3 files changed, 136 insertions(+), 21 deletions(-) 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")