[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
This commit is contained in:
Pedro M. Baeza
2023-02-11 14:01:38 +01:00
parent 9037bece23
commit e1e1b7a0a6
6 changed files with 113 additions and 2 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_move_form" model="ir.ui.view">
<field name="name">account.move.form - Add helper sale_line_ids</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_line_ids']/tree" position="inside">
<field name="sale_line_ids" readonly="0" invisible="1" />
</xpath>
<xpath expr="//field[@name='line_ids']/tree" position="inside">
<field name="sale_line_ids" readonly="0" invisible="1" />
</xpath>
</field>
</record>
</odoo>