From e1e1b7a0a6ad65e4314c2a02681dd2fd99644dfe Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Sat, 11 Feb 2023 14:01:38 +0100 Subject: [PATCH] [FIX+IMP] rma_sale: Link invoice/move line with origin sale line when refunding Steps to reproduce: - Create a sales order with an storable product with invoicing policy on delivered quantities. - Confirm it and deliver the product. - Invoice the order. - Do an RMA, receive it, and refund it. Result: the delivered quantity is 1 instead of 0. This is because the refund generated from the RMA is not linked to sales order line, nor the RMA reception move. This is done because other operations are performed: - Be replaced. - Be changed by other product. And we don't also want that meanwhile the RMA is being performed, the sales order is pending to invoice. But when the refund has been done, we have it clear, so let's link both and have sales statistics correct. FIX: We don't link the refund line with the sales order if the RMA quantity is not the whole original move quantity. Otherwise, we will have incoherente delivered/invoiced quantities on the sales order. TT41645 --- rma_sale/__manifest__.py | 1 + rma_sale/models/__init__.py | 1 + rma_sale/models/account_move.py | 24 +++++++++++ rma_sale/models/rma.py | 59 ++++++++++++++++++++++++++- rma_sale/tests/test_rma_sale.py | 14 ++++++- rma_sale/views/account_move_views.xml | 16 ++++++++ 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 rma_sale/models/account_move.py create mode 100644 rma_sale/views/account_move_views.xml diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py index eec2e878..96de8bc1 100644 --- a/rma_sale/__manifest__.py +++ b/rma_sale/__manifest__.py @@ -13,6 +13,7 @@ "depends": ["rma", "sale_stock"], "data": [ "security/ir.model.access.csv", + "views/account_move_views.xml", "views/report_rma.xml", "views/rma_views.xml", "views/sale_views.xml", diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py index 16972d9b..9047bb78 100644 --- a/rma_sale/models/__init__.py +++ b/rma_sale/models/__init__.py @@ -1,4 +1,5 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import account_move from . import res_company from . import res_config_settings from . import rma diff --git a/rma_sale/models/account_move.py b/rma_sale/models/account_move.py new file mode 100644 index 00000000..e5a5a7a8 --- /dev/null +++ b/rma_sale/models/account_move.py @@ -0,0 +1,24 @@ +# Copyright 2023 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def button_cancel(self): + """If this a refund linked to an RMA, undo the linking of the reception move for + having proper quantities and status. + """ + for rma in self.env["rma"].search([("refund_id", "in", self.ids)]): + if rma.sale_line_id: + rma._unlink_refund_with_reception_move() + return super().button_cancel() + + def button_draft(self): + """Relink the reception move when passing the refund again to draft.""" + for rma in self.env["rma"].search([("refund_id", "in", self.ids)]): + if rma.sale_line_id: + rma._link_refund_with_reception_move() + return super().button_draft() diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py index 9286b05b..31b3254e 100644 --- a/rma_sale/models/rma.py +++ b/rma_sale/models/rma.py @@ -1,7 +1,9 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models +from odoo.tools import float_compare class Rma(models.Model): @@ -41,6 +43,8 @@ class Rma(models.Model): domain="order_id and [('id', 'in', allowed_product_ids)] or " "[('type', 'in', ['consu', 'product'])]" ) + # Add index to this field, as we perform a search on it + refund_id = fields.Many2one(index=True) @api.depends("partner_id", "order_id") def _compute_allowed_picking_ids(self): @@ -89,6 +93,45 @@ class Rma(models.Model): def _onchange_order_id(self): self.product_id = self.picking_id = False + def _link_refund_with_reception_move(self): + """Perform the internal operations for linking the RMA reception move with the + sales order line if applicable. + """ + self.ensure_one() + move = self.reception_move_id + if ( + move + and float_compare( + self.product_uom_qty, + move.product_uom_qty, + precision_rounding=move.product_uom.rounding, + ) + == 0 + ): + self.reception_move_id.sale_line_id = self.sale_line_id.id + self.reception_move_id.to_refund = True + + def _unlink_refund_with_reception_move(self): + """Perform the internal operations for unlinking the RMA reception move with the + sales order line. + """ + self.ensure_one() + self.reception_move_id.sale_line_id = False + self.reception_move_id.to_refund = False + + def action_refund(self): + """As we have made a refund, the return move + the refund should be linked to + the source sales order line, to decrease both the delivered and invoiced + quantity. + + NOTE: The refund line is linked to the SO line in `_prepare_refund_line`. + """ + res = super().action_refund() + for rma in self: + if rma.sale_line_id: + rma._link_refund_with_reception_move() + return res + def _prepare_refund(self, invoice_form, origin): """Inject salesman from sales order (if any)""" res = super()._prepare_refund(invoice_form, origin) @@ -110,10 +153,24 @@ class Rma(models.Model): return self.sale_line_id.product_id def _prepare_refund_line(self, line_form): - """Add line data""" + """Add line data and link to the sales order, only if the RMA is for the whole + move quantity. In other cases, incorrect delivered/invoiced quantities will be + logged on the sales order, so better to let the operations not linked. + """ res = super()._prepare_refund_line(line_form) line = self.sale_line_id if line: line_form.discount = line.discount line_form.sequence = line.sequence + move = self.reception_move_id + if ( + move + and float_compare( + self.product_uom_qty, + move.product_uom_qty, + precision_rounding=move.product_uom.rounding, + ) + == 0 + ): + line_form.sale_line_ids.add(line) return res diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py index ad526684..45908b6d 100644 --- a/rma_sale/tests/test_rma_sale.py +++ b/rma_sale/tests/test_rma_sale.py @@ -96,16 +96,28 @@ class TestRmaSale(TestRmaSaleBase): rma.reception_move_id.picking_id + self.order_out_picking, order.picking_ids, ) - # Refund the RMA user = self.env["res.users"].create( {"login": "test_refund_with_so", "name": "Test"} ) order.user_id = user.id + # Receive the RMA rma.action_confirm() rma.reception_move_id.quantity_done = rma.product_uom_qty rma.reception_move_id.picking_id._action_done() + # Refund the RMA rma.action_refund() + self.assertEqual(self.order_line.qty_delivered, 0) + self.assertEqual(self.order_line.qty_invoiced, -5) self.assertEqual(rma.refund_id.user_id, user) + self.assertEqual(rma.refund_id.invoice_line_ids.sale_line_ids, self.order_line) + # Cancel the refund + rma.refund_id.button_cancel() + self.assertEqual(self.order_line.qty_delivered, 5) + self.assertEqual(self.order_line.qty_invoiced, 0) + # And put it to draft again + rma.refund_id.button_draft() + self.assertEqual(self.order_line.qty_delivered, 0) + self.assertEqual(self.order_line.qty_invoiced, -5) @users("partner@rma") def test_create_rma_from_so_portal_user(self): diff --git a/rma_sale/views/account_move_views.xml b/rma_sale/views/account_move_views.xml new file mode 100644 index 00000000..96dd435b --- /dev/null +++ b/rma_sale/views/account_move_views.xml @@ -0,0 +1,16 @@ + + + + account.move.form - Add helper sale_line_ids + account.move + + + + + + + + + + +