From c9941eb003db72df96f07c57aeb01d58443ef1a8 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Fri, 26 Feb 2021 15:38:24 +0100 Subject: [PATCH] [13.0][IMP] stock_auto_move: Take into account cases with mixed moves (take2) In case of partial flows with move cancel and mixed moves (auto and manual), we need to create a backorder for manual quantities to not get a picking with moves with mixed states (done, cancel, assigned). We remove code that is never called on picking side --- stock_auto_move/models/__init__.py | 1 - stock_auto_move/models/stock_move.py | 17 +-- stock_auto_move/models/stock_picking.py | 46 ------- stock_auto_move/tests/test_stock_auto_move.py | 125 ++++++++++++++++++ 4 files changed, 127 insertions(+), 62 deletions(-) delete mode 100644 stock_auto_move/models/stock_picking.py diff --git a/stock_auto_move/models/__init__.py b/stock_auto_move/models/__init__.py index ce7a5ed78790..144c5d760cf7 100644 --- a/stock_auto_move/models/__init__.py +++ b/stock_auto_move/models/__init__.py @@ -1,3 +1,2 @@ from . import stock_move from . import stock_rule -from . import stock_picking diff --git a/stock_auto_move/models/stock_move.py b/stock_auto_move/models/stock_move.py index 5f5e1200ef1b..99216ad90fc1 100644 --- a/stock_auto_move/models/stock_move.py +++ b/stock_auto_move/models/stock_move.py @@ -29,22 +29,9 @@ def _action_assign(self): moves._action_done( cancel_backorder=self.env.context.get("cancel_backorder", False) ) - already_assigned_moves = self.filtered(lambda m: m.state == "assigned") + # We need to create backorder if there are mixed moves (auto and manual) + moves.mapped("picking_id")._create_backorder() - not_assigned_auto_move = self - already_assigned_moves - - # Process only moves that have been processed recently - auto_moves = not_assigned_auto_move.filtered( - lambda m: m.state in ("assigned", "partially_available") and m.auto_move - ) - - # group the moves by pickings - auto_moves_by_pickings = self._get_auto_moves_by_pickings(auto_moves) - - # process the moves by creating backorders - self.env["stock.picking"]._transfer_pickings_with_auto_move( - auto_moves_by_pickings - ) return res @api.model diff --git a/stock_auto_move/models/stock_picking.py b/stock_auto_move/models/stock_picking.py deleted file mode 100644 index eb1b1e7a99aa..000000000000 --- a/stock_auto_move/models/stock_picking.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2017 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api, models - - -class StockPicking(models.Model): - - _inherit = "stock.picking" - - @api.model - def _transfer_pickings_with_auto_move(self, auto_moves_by_pickings): - """This function is meant to simulate what a user would normally - transfer a picking from the user interface either partial processing - or full processing. - @params auto_moves_by_pickings: dict of moves grouped by pickings - {stock.picking(id): stock.move(id1, id2, id3 ...), ...} - """ - backorder_obj = self.env["stock.backorder.confirmation"] - immediate_obj = self.env["stock.immediate.transfer"] - for picking in auto_moves_by_pickings: - # Create immediate transfer wizard so it will fill the qty_done - # on the auto move linked operation - immediate_wizard_dict = picking.button_validate() - if ( - isinstance(immediate_wizard_dict, dict) - and "res_model" in immediate_wizard_dict - and "stock.immediate.transfer" == immediate_wizard_dict.get("res_model") - ): - immediate_wizard = immediate_obj.browse( - immediate_wizard_dict.get("res_id") - ) - backorder_wizard_dict = immediate_wizard.process() - if ( - isinstance(backorder_wizard_dict, dict) - and "res_model" in backorder_wizard_dict - and "stock.backorder.confirmation" - == backorder_wizard_dict.get("res_model") - ): - backorder_wizard = backorder_obj.browse( - backorder_wizard_dict.get("res_id") - ) - if backorder_wizard: - backorder_wizard.process() - - return diff --git a/stock_auto_move/tests/test_stock_auto_move.py b/stock_auto_move/tests/test_stock_auto_move.py index 5699fe7216c4..6dd2254d3096 100644 --- a/stock_auto_move/tests/test_stock_auto_move.py +++ b/stock_auto_move/tests/test_stock_auto_move.py @@ -21,6 +21,7 @@ def setUpClass(cls): cls.picking_type_id = cls.env.ref("stock.picking_type_internal").id cls.auto_group = cls.env.ref("stock_auto_move.automatic_group") cls.move_obj = cls.env["stock.move"] + cls.procurement_group_obj = cls.env["procurement.group"] def test_10_auto_move(self): """Check automatic processing of move with auto_move set.""" @@ -535,3 +536,127 @@ def test_90_partial_chained_auto_move_no_backorder(self): self.assertEquals("done", move2.move_dest_ids.state) self.assertEquals(2.0, move1.move_dest_ids.quantity_done) self.assertEquals(1.0, move2.move_dest_ids.quantity_done) + + def test_100_partial_chained_auto_move_mixed_no_backorder(self): + """ + Test case: + We do a two steps picking flow mixing products with auto move + and no auto move. + The procurement group is the same to simulate a picking flow + for a purchase for instance. + We transfer the first picking with partial quantities for both + products and do not require backorder. + Expected Result: + The second step picking should be done for the auto move product + and a backorder should have been generated for not yet transfered + products. Indeed, a picking should not contain done movements with + not yet done ones (and not cancelled). + + PICKING 1 PICKING 2 + PRODUCT 1 (AUTO): 10 (5 done) => PRODUCT 1 (AUTO): 10 + (5 done, 5 cancelled) + ^ + | (backorder) + PICKING 3 + PRODUCT 2: 10 (5 done) => PRODUCT 2: 10 (5 partially ready) + """ + vals = {"name": "PROCUREMENT PURCHASE TEST"} + self.procurement = self.procurement_group_obj.create(vals) + warehouse = self.env.ref("stock.warehouse0") + warehouse.reception_steps = "two_steps" + warehouse.reception_route_id.rule_ids.action = "push" + warehouse.reception_route_id.rule_ids.auto_move = True + warehouse.int_type_id.use_create_lots = False + warehouse.int_type_id.use_existing_lots = True + + # Create a second route for non automatic products + route_manual = warehouse.reception_route_id.copy() + route_manual.rule_ids.auto_move = False + + # Set product_2 to manual + self.product_2.route_ids -= warehouse.reception_route_id + self.product_2.route_ids |= route_manual + + picking = ( + self.env["stock.picking"] + .with_context(default_picking_type_id=warehouse.in_type_id.id) + .create( + { + "partner_id": self.env.ref("base.res_partner_1").id, + "picking_type_id": warehouse.in_type_id.id, + "group_id": self.procurement.id, + "location_id": self.env.ref("stock.stock_location_suppliers").id, + } + ) + ) + + move1 = self.env["stock.move"].create( + { + "name": "Supply source location for test", + "product_id": self.product_a1232.id, + "product_uom": self.product_uom_unit_id, + "product_uom_qty": 10, + "picking_id": picking.id, + "location_id": self.env.ref("stock.stock_location_suppliers").id, + "location_dest_id": warehouse.wh_input_stock_loc_id.id, + "picking_type_id": warehouse.in_type_id.id, + "propagate_cancel": True, + "group_id": self.procurement.id, + } + ) + + move2 = self.env["stock.move"].create( + { + "name": "Supply source location for test", + "product_id": self.product_2.id, + "product_uom": self.product_uom_unit_id, + "product_uom_qty": 10, + "picking_id": picking.id, + "location_id": self.env.ref("stock.stock_location_suppliers").id, + "location_dest_id": warehouse.wh_input_stock_loc_id.id, + "picking_type_id": warehouse.in_type_id.id, + "propagate_cancel": True, + "group_id": self.procurement.id, + } + ) + + picking.action_confirm() + self.assertTrue(move1.move_dest_ids.auto_move) + self.assertFalse(move2.move_dest_ids.auto_move) + second_step_picking = move2.move_dest_ids.picking_id + + # do partial reception of the first picking + move1.move_line_ids.qty_done = 5 + move1.move_line_ids.product_uom_qty = 5 + + move2.move_line_ids.qty_done = 5 + move2.move_line_ids.product_uom_qty = 5 + + res = picking.button_validate() + self.assertDictContainsSubset( + {"res_model": "stock.backorder.confirmation"}, res, + ) + wizard = self.env["stock.backorder.confirmation"].browse(res["res_id"]) + wizard.process_cancel_backorder() + + # We need to ensure that all moves are done or cancelled in the + # second picking + self.assertItemsEqual( + ["done", "cancel"], + list(set(second_step_picking.move_lines.mapped("state"))), + ) + + # The second step picking should have a backorder for the + # manual products + second_step_back_order = self.env["stock.picking"].search( + [("backorder_id", "=", second_step_picking.id)] + ) + + self.assertTrue(second_step_back_order) + # If https://github.com/odoo/odoo/pull/66124 is integrated, + # this should become assigned as remaining quantity should be cancelled + # and quantities should be 5.0 + self.assertEquals( + "partially_available", second_step_back_order.move_lines.state + ) + self.assertEquals(10.0, second_step_back_order.move_lines.product_uom_qty)