mirror of
https://github.com/OCA/rma.git
synced 2025-02-16 17:11:47 +02:00
[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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
24
rma_sale/models/account_move.py
Normal file
24
rma_sale/models/account_move.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
16
rma_sale/views/account_move_views.xml
Normal file
16
rma_sale/views/account_move_views.xml
Normal 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>
|
||||
Reference in New Issue
Block a user