From dd29c431e91490c64d7994c2079b0c4bb9541db0 Mon Sep 17 00:00:00 2001 From: Jordi Ballester Alomar Date: Thu, 24 Nov 2022 18:04:05 +0100 Subject: [PATCH] [FIX] fix rma management using multi step routes --- rma/models/rma_order_line.py | 20 +++- rma/models/stock_move.py | 6 ++ rma/models/stock_rule.py | 5 + rma/tests/test_rma.py | 109 ++++++++++++++++---- rma/wizards/rma_make_picking.py | 3 +- rma_account/tests/test_rma_stock_account.py | 84 +++++++++++++++ 6 files changed, 203 insertions(+), 24 deletions(-) diff --git a/rma/models/rma_order_line.py b/rma/models/rma_order_line.py index 82d80b40..cb73c55e 100644 --- a/rma/models/rma_order_line.py +++ b/rma/models/rma_order_line.py @@ -70,6 +70,18 @@ class RmaOrderLine(models.Model): moves |= moves return moves + @api.model + def _get_out_moves(self): + moves = self.env["stock.move"] + for move in self.move_ids: + first_usage = move._get_first_usage() + last_usage = move._get_last_usage() + if first_usage == "internal" and last_usage != "internal": + moves |= move + elif first_usage == "supplier" and last_usage == "customer": + moves |= moves + return moves + @api.model def _get_out_pickings(self): pickings = self.env["stock.picking"] @@ -97,12 +109,10 @@ class RmaOrderLine(models.Model): product_obj = self.env["uom.uom"] qty = 0.0 if direction == "in": - op = ops["="] + moves = rec._get_in_moves() else: - op = ops["!="] - for move in rec.move_ids.filtered( - lambda m: m.state in states and op(m.location_id.usage, rec.type) - ): + moves = rec._get_out_moves() + for move in moves.filtered(lambda m: m.state in states): # If the move is part of a chain don't count it if direction == "out" and move.move_orig_ids: continue diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py index c17b0170..4b675429 100644 --- a/rma/models/stock_move.py +++ b/rma/models/stock_move.py @@ -104,3 +104,9 @@ class StockMove(models.Model): def _prepare_merge_moves_distinct_fields(self): res = super()._prepare_merge_moves_distinct_fields() return res + ["rma_line_id"] + + def _prepare_procurement_values(self): + self.ensure_one() + res = super(StockMove, self)._prepare_procurement_values() + res["rma_line_id"] = self.rma_line_id.id + return res diff --git a/rma/models/stock_rule.py b/rma/models/stock_rule.py index 0c7119f5..d167fcdf 100644 --- a/rma/models/stock_rule.py +++ b/rma/models/stock_rule.py @@ -28,9 +28,14 @@ class StockRule(models.Model): company_id, values, ) + line = self.env["rma.order.line"] if "rma_line_id" in values: line = values.get("rma_line_id") line = self.env["rma.order.line"].browse([line]) + elif "group_id" in values: + pg = values["group_id"] + line = pg.rma_line_id + if line: res["rma_line_id"] = line.id if line.delivery_address_id: res["partner_id"] = line.delivery_address_id.id diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index d3768252..0a3dcf35 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -26,6 +26,9 @@ class TestRma(common.SavepointCase): cls.rma_cust_replace_op_id = cls.env.ref("rma.rma_operation_customer_replace") cls.rma_sup_replace_op_id = cls.env.ref("rma.rma_operation_supplier_replace") cls.rma_ds_replace_op_id = cls.env.ref("rma.rma_operation_ds_replace") + cls.customer_route = cls.env.ref("rma.route_rma_customer") + cls.input_location = cls.env.ref("stock.stock_location_company") + cls.output_location = cls.env.ref("stock.stock_location_output") cls.category = cls._create_product_category( "one_step", cls.rma_cust_replace_op_id, cls.rma_sup_replace_op_id ) @@ -82,7 +85,7 @@ class TestRma(common.SavepointCase): @classmethod def _create_user(cls, login, groups, company): group_ids = [group.id for group in groups] - user = cls.res_users_model.create( + user = cls.res_users_model.with_context(no_reset_password=True).create( { "name": login, "login": login, @@ -105,13 +108,15 @@ class TestRma(common.SavepointCase): } ).create({}) wizard._create_picking() - res = rma_line_ids.action_view_in_shipments() - picking = cls.env["stock.picking"].browse(res["res_id"]) - picking.action_assign() - for mv in picking.move_lines: - mv.quantity_done = mv.product_uom_qty - picking._action_done() - return picking + pickings = rma_line_ids._get_in_pickings() + pickings.action_assign() + for picking in pickings: + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + # In case of two step pickings, ship in two steps: + while pickings.filtered(lambda p: p.state == "assigned"): + pickings._action_done() + return pickings @classmethod def _deliver_rma(cls, rma_line_ids): @@ -124,13 +129,13 @@ class TestRma(common.SavepointCase): } ).create({}) wizard._create_picking() - res = rma_line_ids.action_view_out_shipments() - picking = cls.env["stock.picking"].browse(res["res_id"]) - picking.action_assign() - for mv in picking.move_lines: - mv.quantity_done = mv.product_uom_qty - picking._action_done() - return picking + pickings = rma_line_ids._get_out_pickings() + pickings.action_assign() + for picking in pickings: + for mv in picking.move_lines: + mv.quantity_done = mv.product_uom_qty + pickings._action_done() + return pickings @classmethod def _create_product_category( @@ -773,9 +778,7 @@ class TestRma(common.SavepointCase): } ).create({}) wizard._create_picking() - res = self.rma_supplier_id.rma_line_ids.action_view_out_shipments() - self.assertTrue("res_id" in res, "Incorrect number of pickings" "created") - picking = self.env["stock.picking"].browse(res["res_id"]) + picking = self.rma_supplier_id.rma_line_ids._get_out_pickings() moves = picking.move_lines self.assertEqual(len(moves), 3, "Incorrect number of moves created") @@ -1073,3 +1076,73 @@ class TestRma(common.SavepointCase): ).create({}) with self.assertRaisesRegex(ValidationError, "No quantity to transfer"): wizard._create_picking() + + def test_08_customer_rma_multi_step(self): + """ + Receive a product and then return it using a multi-step route. + """ + # Alter the customer RMA route to make it multi-step + # Get rid of the duplicated rule + self.env.ref("rma.rule_rma_customer_out_pull").active = False + self.env.ref("rma.rule_rma_customer_in_pull").active = False + cust_in_pull_rule = self.customer_route.rule_ids.filtered( + lambda r: r.location_id == self.stock_rma_location + ) + cust_in_pull_rule.location_id = self.input_location + cust_out_pull_rule = self.customer_route.rule_ids.filtered( + lambda r: r.location_src_id == self.env.ref("rma.location_rma") + ) + cust_out_pull_rule.location_src_id = self.output_location + cust_out_pull_rule.procure_method = "make_to_order" + self.env["stock.rule"].create( + { + "name": "RMA->Output", + "action": "pull", + "warehouse_id": self.wh.id, + "location_src_id": self.env.ref("rma.location_rma").id, + "location_id": self.output_location.id, + "procure_method": "make_to_stock", + "route_id": self.customer_route.id, + "picking_type_id": self.env.ref("stock.picking_type_internal").id, + } + ) + self.env["stock.rule"].create( + { + "name": "Output->RMA", + "action": "pull", + "warehouse_id": self.wh.id, + "location_src_id": self.input_location.id, + "location_id": self.env.ref("rma.location_rma").id, + "procure_method": "make_to_order", + "route_id": self.customer_route.id, + "picking_type_id": self.env.ref("stock.picking_type_internal").id, + } + ) + # Set a standard price on the products + self.product_1.standard_price = 10 + self._create_inventory( + self.product_1, 20.0, self.env.ref("stock.stock_location_customers") + ) + products2move = [ + (self.product_1, 3), + ] + self.product_1.categ_id.rma_customer_operation_id = self.rma_cust_replace_op_id + rma_customer_id = self._create_rma_from_move( + products2move, + "customer", + self.env.ref("base.res_partner_2"), + dropship=False, + ) + rma = rma_customer_id.rma_line_ids + rma.action_rma_to_approve() + self.assertEqual(rma.qty_to_receive, 3) + self.assertEqual(rma.qty_received, 0) + self._receive_rma(rma) + self.assertEqual(len(rma.move_ids), 2) + self.assertEqual(rma.qty_to_receive, 0) + self.assertEqual(rma.qty_received, 3) + self.assertEqual(rma.qty_to_deliver, 3) + self._deliver_rma(rma) + self.assertEqual(rma.qty_to_deliver, 0) + self.assertEqual(rma.qty_delivered, 3) + self.assertEqual(len(rma.move_ids), 4) diff --git a/rma/wizards/rma_make_picking.py b/rma/wizards/rma_make_picking.py index c6ec5fcf..602b2595 100644 --- a/rma/wizards/rma_make_picking.py +++ b/rma/wizards/rma_make_picking.py @@ -73,8 +73,9 @@ class RmaMakePicking(models.TransientModel): "partner_id": item.line_id.partner_id.id, "name": item.line_id.rma_id.name or item.line_id.name, "rma_id": item.line_id.rma_id and item.line_id.rma_id.id or False, - "rma_line_id": item.line_id.id if not item.line_id.rma_id else False, } + if not item.line_id.rma_id: + group_data["rma_line_id"] = item.line_id and item.line_id.id or False return group_data @api.model diff --git a/rma_account/tests/test_rma_stock_account.py b/rma_account/tests/test_rma_stock_account.py index 64102366..6b4d92f1 100644 --- a/rma_account/tests/test_rma_stock_account.py +++ b/rma_account/tests/test_rma_stock_account.py @@ -240,3 +240,87 @@ class TestRmaStockAccount(TestRma): self.assertEqual(gdni_balance, 0.0) # The GDNI entries should be now reconciled self.assertEqual(all(gdni_amls.mapped("reconciled")), True) + + def test_04_cost_from_move_multi_step(self): + """ + Receive a product and then return it using a multi-step route. + The Goods Delivered Not Invoiced should result in 0 + """ + # Alter the customer RMA route to make it multi-step + # Get rid of the duplicated rule + self.env.ref("rma.rule_rma_customer_out_pull").active = False + self.env.ref("rma.rule_rma_customer_in_pull").active = False + cust_in_pull_rule = self.customer_route.rule_ids.filtered( + lambda r: r.location_id == self.stock_rma_location + ) + cust_in_pull_rule.location_id = self.input_location + cust_out_pull_rule = self.customer_route.rule_ids.filtered( + lambda r: r.location_src_id == self.env.ref("rma.location_rma") + ) + cust_out_pull_rule.location_src_id = self.output_location + cust_out_pull_rule.procure_method = "make_to_order" + self.env["stock.rule"].create( + { + "name": "RMA->Output", + "action": "pull", + "warehouse_id": self.wh.id, + "location_src_id": self.env.ref("rma.location_rma").id, + "location_id": self.output_location.id, + "procure_method": "make_to_stock", + "route_id": self.customer_route.id, + "picking_type_id": self.env.ref("stock.picking_type_internal").id, + } + ) + self.env["stock.rule"].create( + { + "name": "Output->RMA", + "action": "pull", + "warehouse_id": self.wh.id, + "location_src_id": self.input_location.id, + "location_id": self.env.ref("rma.location_rma").id, + "procure_method": "make_to_order", + "route_id": self.customer_route.id, + "picking_type_id": self.env.ref("stock.picking_type_internal").id, + } + ) + # Set a standard price on the products + self.product_fifo_1.standard_price = 10 + self._create_inventory( + self.product_fifo_1, 20.0, self.env.ref("stock.stock_location_customers") + ) + products2move = [ + (self.product_fifo_1, 3), + ] + self.product_fifo_1.categ_id.rma_customer_operation_id = ( + self.rma_cust_replace_op_id + ) + rma_customer_id = self._create_rma_from_move( + products2move, + "customer", + self.env.ref("base.res_partner_2"), + dropship=False, + ) + # Set an incorrect price in the RMA (this should not affect cost) + rma = rma_customer_id.rma_line_ids + rma.price_unit = 999 + rma.action_rma_to_approve() + self._receive_rma(rma) + layers = rma.move_ids.sudo().stock_valuation_layer_ids + gdni_amls = layers.account_move_id.line_ids.filtered( + lambda l: l.account_id == self.account_gdni + ) + gdni_balance = sum(gdni_amls.mapped("balance")) + self.assertEqual(len(gdni_amls), 1) + # Balance should be -30, as we have only received + self.assertEqual(gdni_balance, -30.0) + self._deliver_rma(rma) + layers = rma.move_ids.sudo().stock_valuation_layer_ids + gdni_amls = layers.account_move_id.line_ids.filtered( + lambda l: l.account_id == self.account_gdni + ) + gdni_balance = sum(gdni_amls.mapped("balance")) + self.assertEqual(len(gdni_amls), 2) + # Balance should be 0, as we have received and shipped + self.assertEqual(gdni_balance, 0.0) + # The GDNI entries should be now reconciled + self.assertEqual(all(gdni_amls.mapped("reconciled")), True)