OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
diff --git a/rma/tests/__init__.py b/rma/tests/__init__.py
index 5f9ab818b..8556dc3aa 100644
--- a/rma/tests/__init__.py
+++ b/rma/tests/__init__.py
@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_rma
+from . import test_rma_operation
diff --git a/rma/tests/test_rma_operation.py b/rma/tests/test_rma_operation.py
new file mode 100644
index 000000000..ab1478df0
--- /dev/null
+++ b/rma/tests/test_rma_operation.py
@@ -0,0 +1,304 @@
+# Copyright 2020 Tecnativa - Ernesto Tejeda
+# Copyright 2023 Michael Tietz (MT Software)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo.exceptions import ValidationError
+from odoo.tests.common import Form
+
+from .test_rma import TestRma
+
+
+class TestRmaOperation(TestRma):
+ def test_01(self):
+ """
+ ensure that the receipt creation behaves correctly according to the
+ action_create_receipt setting.
+ - "automatic_on_confirm":
+ - receipts are created automatically
+ - the manual button is hidden
+ - "manual_on_confirm"
+ - manual button is visible after confirmation
+ - disappears once a receipt is manually created
+ """
+ self.assertEqual(self.operation.action_create_receipt, "automatic_on_confirm")
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.show_create_receipt)
+ rma.action_confirm()
+ self.assertTrue(rma.reception_move_id)
+ self.assertFalse(rma.show_create_receipt)
+ self.operation.action_create_receipt = "manual_on_confirm"
+ rma2 = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma2.action_confirm()
+ self.assertTrue(rma2.show_create_receipt)
+ self.assertFalse(rma2.reception_move_id)
+ rma2.action_create_receipt()
+ self.assertFalse(rma2.show_create_receipt)
+
+ def test_02(self):
+ """
+ test delivery button visibility based on operation settings.
+ No deliver possible
+ """
+ self.operation.action_create_delivery = False
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.can_be_replaced)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.show_create_return)
+ self.assertFalse(rma.can_be_replaced)
+ self.assertFalse(rma.show_create_replace)
+
+ def test_03(self):
+ """
+ test delivery button visibility based on operation settings.
+ deliver manually after confirm
+ """
+ self.operation.action_create_delivery = "manual_on_confirm"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.can_be_replaced)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertTrue(rma.can_be_returned)
+ self.assertTrue(rma.show_create_return)
+ self.assertTrue(rma.can_be_replaced)
+ self.assertTrue(rma.show_create_replace)
+
+ def test_04(self):
+ """
+ test delivery button visibility based on operation settings.
+ deliver automatically after confirm, return same product
+ """
+ self.operation.action_create_delivery = "automatic_on_confirm"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.can_be_replaced)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "waiting_replacement")
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.show_create_return)
+ self.assertFalse(rma.can_be_replaced)
+ self.assertFalse(rma.show_create_replace)
+ self.assertTrue(rma.delivery_move_ids)
+ self.assertEqual(rma.delivery_move_ids.product_id, self.product)
+ self.assertEqual(rma.delivery_move_ids.product_uom_qty, 10)
+
+ def test_05(self):
+ """
+ test delivery button visibility based on operation settings.
+ deliver manually after receipt
+ """
+ self.operation.action_create_delivery = "manual_after_receipt"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.can_be_replaced)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.show_create_return)
+ self.assertFalse(rma.can_be_replaced)
+ self.assertFalse(rma.show_create_replace)
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(rma.state, "received")
+ self.assertTrue(rma.can_be_returned)
+ self.assertTrue(rma.show_create_return)
+ self.assertTrue(rma.can_be_replaced)
+ self.assertTrue(rma.show_create_replace)
+
+ def test_06(self):
+ """
+ test delivery button visibility based on operation settings.
+ deliver automatically after receipt
+ """
+ self.operation.action_create_delivery = "automatic_after_receipt"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.can_be_replaced)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.show_create_return)
+ self.assertFalse(rma.can_be_replaced)
+ self.assertFalse(rma.show_create_replace)
+ self.assertFalse(rma.delivery_move_ids)
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(rma.delivery_move_ids.product_id, self.product)
+ self.assertEqual(rma.delivery_move_ids.product_uom_qty, 10)
+ self.assertEqual(rma.state, "waiting_replacement")
+ self.assertFalse(rma.can_be_returned)
+ self.assertFalse(rma.show_create_return)
+ self.assertTrue(rma.can_be_replaced)
+ self.assertFalse(rma.show_create_replace)
+
+ def test_07(self):
+ """
+ test delivery button visibility based on operation settings.
+ deliver automatically after confirm, different product
+ """
+ self.operation.action_create_delivery = "automatic_on_confirm"
+ self.operation.different_return_product = True
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ with self.assertRaises(AssertionError, msg="Replacement fields are required"):
+ with Form(rma) as rma_form:
+ rma_form.save()
+ with self.assertRaises(
+ ValidationError, msg="Complete the replacement information"
+ ):
+ rma.action_confirm()
+ rma.return_product_id = self.product_product.create(
+ {"name": "return Product test 1", "type": "product"}
+ )
+ rma.action_confirm()
+ self.assertEqual(rma.delivery_move_ids.product_id, rma.product_id)
+ self.assertEqual(rma.reception_move_id.product_id, rma.return_product_id)
+ self.assertEqual(rma.state, "waiting_replacement")
+
+ def test_08(self):
+ """test refund, manually after confirm"""
+ self.operation.action_create_refund = "manual_on_confirm"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertTrue(rma.can_be_refunded)
+ self.assertTrue(rma.show_create_refund)
+
+ def test_09(self):
+ """test refund, manually after receipt"""
+ self.operation.action_create_refund = "manual_after_receipt"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ self.assertFalse(rma.can_be_refunded)
+ self.assertFalse(rma.show_create_refund)
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(rma.state, "received")
+ self.assertTrue(rma.can_be_refunded)
+ self.assertTrue(rma.show_create_refund)
+
+ def test_10(self):
+ """test refund, automatic after confirm"""
+ self.operation.action_create_refund = "automatic_on_confirm"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "refunded")
+ self.assertTrue(rma.refund_id)
+ self.assertFalse(rma.can_be_refunded)
+ self.assertFalse(rma.show_create_refund)
+
+ def test_11(self):
+ """test refund, automatic after confirm"""
+ self.operation.action_create_refund = "automatic_after_receipt"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "confirmed")
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(rma.state, "refunded")
+ self.assertTrue(rma.refund_id)
+ self.assertFalse(rma.can_be_refunded)
+ self.assertFalse(rma.show_create_refund)
+
+ def test_12(self):
+ """
+ Refund without product return
+ Some companies may offer refunds without requiring the return of the product,
+ often in cases of low-value items or when the cost of return shipping is
+ prohibitive.
+ - no receipt
+ - no return
+ - automatically refund on confirm
+ """
+ self.operation.action_create_receipt = False
+ self.operation.action_create_refund = "automatic_on_confirm"
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "refunded")
+ self.assertFalse(rma.reception_move_id)
+ self.assertTrue(rma.refund_id)
+
+ def test_13(self):
+ """
+ Return of non-ordered product
+ Occasionally, customers receive items they did not order and need a process for
+ returning these products. The delivered product don't figure on the sale order
+ - receipt
+ - no return
+ - no refund
+ """
+ self.operation.action_create_receipt = "automatic_on_confirm"
+ self.operation.action_create_delivery = False
+ self.operation.action_create_refund = False
+ rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
+ rma.action_confirm()
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(rma.state, "received")
+ self.assertFalse(rma.delivery_move_ids)
+
+ def test_14(self):
+ """if the refund action is not ment to update quantity, return picking line
+ to_refund field should be False"""
+ self.operation.action_create_refund = "manual_after_receipt"
+ origin_delivery = self._create_delivery()
+ stock_return_picking_form = Form(
+ self.env["stock.return.picking"].with_context(
+ active_ids=origin_delivery.ids,
+ active_id=origin_delivery.id,
+ active_model="stock.picking",
+ )
+ )
+ stock_return_picking_form.create_rma = True
+ stock_return_picking_form.rma_operation_id = self.operation
+ return_wizard = stock_return_picking_form.save()
+ return_line = return_wizard.product_return_moves.filtered(
+ lambda m, p=self.product: m.product_id == p
+ )
+ self.assertEqual(return_line.rma_operation_id, self.operation)
+ picking_action = return_wizard.create_returns()
+ reception = self.env["stock.picking"].browse(picking_action["res_id"])
+ move = reception.move_ids.filtered(lambda m, p=self.product: m.product_id == p)
+ self.assertFalse(move.to_refund)
+
+ def test_15(self):
+ """if the refund action is ment to update quantity, return picking line
+ to_refund field should be True"""
+ self.operation.action_create_refund = "update_quantity"
+ origin_delivery = self._create_delivery()
+ stock_return_picking_form = Form(
+ self.env["stock.return.picking"].with_context(
+ active_ids=origin_delivery.ids,
+ active_id=origin_delivery.id,
+ active_model="stock.picking",
+ )
+ )
+ stock_return_picking_form.create_rma = True
+ stock_return_picking_form.rma_operation_id = self.operation
+ return_wizard = stock_return_picking_form.save()
+ return_line = return_wizard.product_return_moves.filtered(
+ lambda m, p=self.product: m.product_id == p
+ )
+ self.assertEqual(return_line.rma_operation_id, self.operation)
+ picking_action = return_wizard.create_returns()
+ reception = self.env["stock.picking"].browse(picking_action["res_id"])
+ move = reception.move_ids.filtered(lambda m, p=self.product: m.product_id == p)
+ self.assertTrue(move.to_refund)
+
+ def test_rma_replace_pick_ship(self):
+ self.operation.action_create_delivery = "automatic_on_confirm"
+ self.warehouse.write({"delivery_steps": "pick_ship"})
+ rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
+ rma.action_confirm()
+ self.assertEqual(rma.state, "waiting_replacement")
+ out_pickings = rma.mapped("delivery_move_ids.picking_id")
+ self.assertEqual(rma.delivery_picking_count, 2)
+ self.assertIn(
+ self.warehouse.pick_type_id, out_pickings.mapped("picking_type_id")
+ )
+ self.assertIn(
+ self.warehouse.out_type_id, out_pickings.mapped("picking_type_id")
+ )
diff --git a/rma/views/rma_operation.xml b/rma/views/rma_operation.xml
new file mode 100644
index 000000000..1ed1d82f6
--- /dev/null
+++ b/rma/views/rma_operation.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+ rma.operation
+
+
+
+
+
+
+ rma.operation
+
+
+
+
+
+
+
+
+ rma.operation
+
+
+
+
+
+
+
+
+
+
+
+
+ Operations
+ rma.operation
+ tree,form
+ []
+ {}
+
+
+
+ Operations
+
+
+
+
+
+
diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml
index 8ad666162..b95aaaeb1 100644
--- a/rma/views/rma_views.xml
+++ b/rma/views/rma_views.xml
@@ -136,25 +136,32 @@
states="draft"
class="btn-primary"
/>
+
+
@@ -341,9 +353,11 @@
-
-
-
+
+
+
+
+
diff --git a/rma/wizard/stock_picking_return.py b/rma/wizard/stock_picking_return.py
index 3bed8265a..cf9fe49fd 100644
--- a/rma/wizard/stock_picking_return.py
+++ b/rma/wizard/stock_picking_return.py
@@ -18,6 +18,14 @@ class ReturnPickingLine(models.TransientModel):
store=True,
readonly=False,
)
+ return_product_id = fields.Many2one(
+ "product.product",
+ help="Product to be returned if it's different from the originally delivered "
+ "item.",
+ )
+ different_return_product = fields.Boolean(
+ related="rma_operation_id.different_return_product"
+ )
@api.depends("wizard_id.rma_operation_id")
def _compute_rma_operation_id(self):
@@ -34,6 +42,7 @@ def _prepare_rma_vals(self):
"product_uom": self.product_id.uom_id.id,
"location_id": self.wizard_id.location_id.id or self.move_id.location_id.id,
"operation_id": self.rma_operation_id.id,
+ "return_product_id": self.return_product_id.id,
}
diff --git a/rma/wizard/stock_picking_return_views.xml b/rma/wizard/stock_picking_return_views.xml
index ce127da35..8be27a19b 100644
--- a/rma/wizard/stock_picking_return_views.xml
+++ b/rma/wizard/stock_picking_return_views.xml
@@ -13,6 +13,11 @@
name="rma_operation_id"
attrs="{'column_invisible': [('parent.create_rma', '=', False)], 'required': [('parent.create_rma', '=', True), ('quantity', '>', 0)]}"
/>
+
+
diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py
index 7662ba5fa..0da46b5aa 100644
--- a/rma_sale/models/rma.py
+++ b/rma_sale/models/rma.py
@@ -175,3 +175,36 @@ def _prepare_delivery_procurements(self, scheduled_date=None, qty=None, uom=None
return super()._prepare_delivery_procurements(
scheduled_date=scheduled_date, qty=qty, uom=uom
)
+
+ def _prepare_delivery_procurement_vals(self, scheduled_date=None):
+ vals = super()._prepare_delivery_procurement_vals(scheduled_date=scheduled_date)
+ if (
+ self.move_id
+ and self.move_id.sale_line_id
+ and self.operation_id.action_create_refund == "update_quantity"
+ ):
+ vals["sale_line_id"] = self.move_id.sale_line_id.id
+ return vals
+
+ def _prepare_replace_procurement_vals(self, warehouse=None, scheduled_date=None):
+ vals = super()._prepare_replace_procurement_vals(
+ warehouse=warehouse, scheduled_date=scheduled_date
+ )
+ if (
+ self.move_id
+ and self.move_id.sale_line_id
+ and self.operation_id.action_create_refund == "update_quantity"
+ ):
+ vals["sale_line_id"] = self.move_id.sale_line_id.id
+ return vals
+
+ def _prepare_reception_procurement_vals(self, group=None):
+ """This method is used only for reception and a specific RMA IN route."""
+ vals = super()._prepare_reception_procurement_vals(group=group)
+ if (
+ self.move_id
+ and self.move_id.sale_line_id
+ and self.operation_id.action_create_refund == "update_quantity"
+ ):
+ vals["sale_line_id"] = self.move_id.sale_line_id.id
+ return vals
diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py
index 532230d1d..003340c86 100644
--- a/rma_sale/tests/test_rma_sale.py
+++ b/rma_sale/tests/test_rma_sale.py
@@ -3,6 +3,7 @@
# Copyright 2023 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo.exceptions import ValidationError
from odoo.tests import Form, TransactionCase
from odoo.tests.common import users
@@ -222,3 +223,70 @@ def test_report_rma(self):
res = str(res[0])
self.assertRegex(res, self.sale_order.name)
self.assertRegex(res, operation.name)
+
+ def test_manual_refund_no_quantity_impact(self):
+ """If the operation is meant for a manual refund, the delivered quantity
+ should not be updated."""
+ self.operation.action_create_refund = "manual_after_receipt"
+ order = self.sale_order
+ order_line = order.order_line
+ self.assertEqual(order_line.qty_delivered, 5)
+ wizard = self._rma_sale_wizard(order)
+ rma = self.env["rma"].browse(wizard.create_and_open_rma()["res_id"])
+ self.assertFalse(rma.reception_move_id.sale_line_id)
+ rma.action_confirm()
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(order.order_line.qty_delivered, 5)
+
+ def test_no_manual_refund_quantity_impact(self):
+ """If the operation is meant for a manual refund, the delivered quantity
+ should not be updated."""
+ self.operation.action_create_refund = "update_quantity"
+ order = self.sale_order
+ order_line = order.order_line
+ self.assertEqual(order_line.qty_delivered, 5)
+ wizard = self._rma_sale_wizard(order)
+ rma = self.env["rma"].browse(wizard.create_and_open_rma()["res_id"])
+ self.assertEqual(rma.reception_move_id.sale_line_id, order_line)
+ self.assertFalse(rma.can_be_refunded)
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(order.order_line.qty_delivered, 0)
+ delivery_form = Form(
+ self.env["rma.delivery.wizard"].with_context(
+ active_ids=rma.ids,
+ rma_delivery_type="return",
+ )
+ )
+ delivery_form.product_uom_qty = rma.product_uom_qty
+ delivery_wizard = delivery_form.save()
+ delivery_wizard.action_deliver()
+ picking = rma.delivery_move_ids.picking_id
+ picking.move_ids.quantity_done = rma.product_uom_qty
+ picking._action_done()
+ self.assertEqual(order.order_line.qty_delivered, 5)
+
+ def test_return_different_product(self):
+ self.operation.action_create_delivery = False
+ self.operation.different_return_product = True
+ self.operation.action_create_refund = "update_quantity"
+ order = self.sale_order
+ order_line = order.order_line
+ self.assertEqual(order_line.qty_delivered, 5)
+ wizard = self._rma_sale_wizard(order)
+ with self.assertRaises(
+ ValidationError, msg="Complete the replacement information"
+ ):
+ rma = self.env["rma"].browse(wizard.create_and_open_rma()["res_id"])
+ return_product = self.product_product.create(
+ {"name": "return Product test 1", "type": "product"}
+ )
+ wizard.line_ids.return_product_id = return_product
+ rma = self.env["rma"].browse(wizard.create_and_open_rma()["res_id"])
+ self.assertEqual(rma.reception_move_id.sale_line_id, order_line)
+ self.assertEqual(rma.reception_move_id.product_id, return_product)
+ self.assertFalse(rma.can_be_refunded)
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id._action_done()
+ self.assertEqual(order.order_line.qty_delivered, 5)
diff --git a/rma_sale/wizard/sale_order_rma_wizard.py b/rma_sale/wizard/sale_order_rma_wizard.py
index 60146c84f..69c35d6a3 100644
--- a/rma_sale/wizard/sale_order_rma_wizard.py
+++ b/rma_sale/wizard/sale_order_rma_wizard.py
@@ -157,6 +157,14 @@ class SaleOrderLineRmaWizard(models.TransientModel):
comodel_name="sale.order.line",
)
description = fields.Text()
+ return_product_id = fields.Many2one(
+ "product.product",
+ help="Product to be returned if it's different from the originally delivered "
+ "item.",
+ )
+ different_return_product = fields.Boolean(
+ related="operation_id.different_return_product"
+ )
@api.depends("wizard_id.operation_id")
def _compute_operation_id(self):
@@ -223,4 +231,5 @@ def _prepare_rma_values(self):
"product_uom": self.uom_id.id,
"operation_id": self.operation_id.id,
"description": description,
+ "return_product_id": self.return_product_id.id,
}
diff --git a/rma_sale/wizard/sale_order_rma_wizard_views.xml b/rma_sale/wizard/sale_order_rma_wizard_views.xml
index 24e0f0cb4..23fe07e54 100644
--- a/rma_sale/wizard/sale_order_rma_wizard_views.xml
+++ b/rma_sale/wizard/sale_order_rma_wizard_views.xml
@@ -35,6 +35,11 @@
name="operation_id"
attrs="{'required': [('quantity', '>', 0)]}"
/>
+
+