diff --git a/rma/__manifest__.py b/rma/__manifest__.py index 14f304282..117e870bd 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -35,6 +35,7 @@ "views/stock_picking_views.xml", "views/stock_warehouse_views.xml", "views/res_config_settings_views.xml", + "views/rma_operation.xml", ], "post_init_hook": "post_init_hook", "application": True, diff --git a/rma/models/rma.py b/rma/models/rma.py index 5921848e9..1fde32ef0 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -317,6 +317,63 @@ def _domain_location_id(self): copy=False, ) + show_create_receipt = fields.Boolean( + string="Show Create Receipt Button", compute="_compute_show_create_receipt" + ) + show_create_return = fields.Boolean( + string="Show Create Return Button", compute="_compute_show_create_return" + ) + show_create_replace = fields.Boolean( + string="Show Create replace Button", compute="_compute_show_create_replace" + ) + show_create_refund = fields.Boolean( + string="Show Create refund Button", compute="_compute_show_refund_replace" + ) + 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("operation_id.action_create_receipt", "state", "reception_move_id") + def _compute_show_create_receipt(self): + for rec in self: + rec.show_create_receipt = ( + not rec.reception_move_id + and rec.operation_id.action_create_receipt == "manual_on_confirm" + and rec.state == "confirmed" + ) + + @api.depends("operation_id.action_create_delivery", "can_be_returned") + def _compute_show_create_return(self): + for rec in self: + rec.show_create_return = ( + rec.operation_id.action_create_delivery + in ("manual_on_confirm", "manual_after_receipt") + and rec.can_be_returned + ) + + @api.depends("operation_id.action_create_delivery", "can_be_replaced") + def _compute_show_create_replace(self): + for rec in self: + rec.show_create_replace = ( + rec.operation_id.action_create_delivery + in ("manual_on_confirm", "manual_after_receipt") + and rec.can_be_replaced + ) + + @api.depends("operation_id.action_create_refund", "can_be_refunded") + def _compute_show_refund_replace(self): + for rec in self: + rec.show_create_refund = ( + rec.operation_id.action_create_refund + in ("manual_on_confirm", "manual_after_receipt") + and rec.can_be_refunded + ) + def _compute_delivery_picking_count(self): for rma in self: rma.delivery_picking_count = len(rma.delivery_move_ids.picking_id) @@ -394,9 +451,17 @@ def _compute_can_be_refunded(self): an rma can be refunded. It is used in rma.action_refund method. """ for record in self: - record.can_be_refunded = record.state == "received" + record.can_be_refunded = ( + record.operation_id.action_create_refund + in ("manual_after_receipt", "automatic_after_receipt") + and record.state == "received" + ) or ( + record.operation_id.action_create_refund + in ("manual_on_confirm", "automatic_on_confirm") + and record.state == "confirmed" + ) - @api.depends("remaining_qty", "state") + @api.depends("remaining_qty", "state", "operation_id.action_create_delivery") def _compute_can_be_returned(self): """Compute 'can_be_returned'. This field controls the visibility of the 'Return to customer' button in the rma form @@ -406,8 +471,17 @@ def _compute_can_be_returned(self): rma._ensure_can_be_returned. """ for r in self: - r.can_be_returned = ( - r.state in ["received", "waiting_return"] and r.remaining_qty > 0 + r.can_be_returned = r.remaining_qty > 0 and ( + ( + r.operation_id.action_create_delivery + in ("manual_after_receipt", "automatic_after_receipt") + and r.state in ["received", "waiting_return"] + ) + or ( + r.operation_id.action_create_delivery + in ("manual_on_confirm", "automatic_on_confirm") + and r.state == "confirmed" + ) ) @api.depends("state") @@ -420,11 +494,20 @@ def _compute_can_be_replaced(self): rma._ensure_can_be_replaced. """ for r in self: - r.can_be_replaced = r.state in [ - "received", - "waiting_replacement", - "replaced", - ] + r.can_be_replaced = ( + r.operation_id.action_create_delivery + in ("manual_after_receipt", "automatic_after_receipt") + and r.state + in [ + "received", + "waiting_replacement", + "replaced", + ] + ) or ( + r.operation_id.action_create_delivery + in ("manual_on_confirm", "automatic_on_confirm") + and r.state == "confirmed" + ) @api.depends("state", "remaining_qty") def _compute_can_be_finished(self): @@ -699,8 +782,11 @@ def _prepare_reception_procurement_vals(self, group=None): vals = self._prepare_common_procurement_vals(group=group) vals["route_ids"] = self.warehouse_id.rma_in_route_id vals["rma_receiver_ids"] = [(6, 0, self.ids)] + vals["to_refund"] = self.operation_id.action_create_refund == "update_quantity" if self.move_id: vals["origin_returned_move_id"] = self.move_id.id + if not self.operation_id.different_return_product: + vals["move_orig_ids"] = [(6, 0, self.move_id.ids)] return vals def _prepare_reception_procurements(self): @@ -712,13 +798,24 @@ def _prepare_reception_procurements(self): group = rma.procurement_group_id if not group: group = group_model.create(rma._prepare_procurement_group_vals()) + product = self.product_id + if self.different_return_product: + if not self.return_product_id: + raise ValidationError( + _( + "The selected operation requires a return product different" + " from the originally delivered item. Please select the " + "product to return." + ) + ) + product = self.return_product_id procurements.append( group_model.Procurement( - rma.product_id, + product, rma.product_uom_qty, rma.product_uom, rma.location_id, - rma.product_id.display_name, + product.display_name, group.name, rma.company_id, rma._prepare_reception_procurement_vals(group), @@ -726,20 +823,51 @@ def _prepare_reception_procurements(self): ) return procurements + def _create_receipt(self): + procurements = self._prepare_reception_procurements() + if procurements: + self.env["procurement.group"].run(procurements) + self.reception_move_id.picking_id.action_assign() + + def action_create_receipt(self): + self.ensure_one() + self._create_receipt() + self.ensure_one() + return { + "name": _("Receipt"), + "type": "ir.actions.act_window", + "view_type": "form", + "view_mode": "form", + "res_model": "stock.picking", + "views": [[False, "form"]], + "res_id": self.reception_move_id.picking_id.id, + } + def action_confirm(self): """Invoked when 'Confirm' button in rma form view is clicked.""" self._ensure_required_fields() self = self.filtered(lambda rma: rma.state == "draft") if not self: return - procurements = self._prepare_reception_procurements() - if procurements: - self.env["procurement.group"].run(procurements) - self.reception_move_id.picking_id.action_assign() self.write({"state": "confirmed"}) for rma in self: rma._add_message_subscribe_partner() self._send_confirmation_email() + for rec in self: + if rec.operation_id.action_create_receipt == "automatic_on_confirm": + rec._create_receipt() + if rec.operation_id.action_create_delivery == "automatic_on_confirm": + rec.with_context( + rma_return_grouping=rec.env.company.rma_return_grouping + ).create_replace( + fields.Datetime.now(), + rec.warehouse_id, + rec.product_id, + rec.product_uom_qty, + rec.product_uom, + ) + if rec.operation_id.action_create_refund == "automatic_on_confirm": + rec.action_refund() def action_refund(self): """Invoked when 'Refund' button in rma form view is clicked @@ -1376,6 +1504,20 @@ def update_received_state_on_reception(self): """ self.write({"state": "received"}) self._send_receipt_confirmation_email() + for rec in self: + if rec.operation_id.action_create_delivery == "automatic_after_receipt": + rec.with_context( + rma_return_grouping=rec.env.company.rma_return_grouping + ).create_replace( + fields.Datetime.now(), + rec.warehouse_id, + rec.product_id, + rec.product_uom_qty, + rec.product_uom, + ) + + if rec.operation_id.action_create_refund == "automatic_after_receipt": + rec.action_refund() def update_received_state(self): """Invoked by: diff --git a/rma/models/rma_operation.py b/rma/models/rma_operation.py index cb4c08bb0..17a05af0e 100644 --- a/rma/models/rma_operation.py +++ b/rma/models/rma_operation.py @@ -10,6 +10,42 @@ class RmaOperation(models.Model): active = fields.Boolean(default=True) name = fields.Char(required=True, translate=True) + action_create_receipt = fields.Selection( + [ + ("manual_on_confirm", "Manually on Confirm"), + ("automatic_on_confirm", "Automatically on Confirm"), + ], + string="Create Receipt", + default="automatic_on_confirm", + help="Define how the receipt action should be handled.", + ) + different_return_product = fields.Boolean( + help="If checked, allows the return of a product different from the one " + "originally ordered. Used if the delivery is created automatically", + ) + action_create_delivery = fields.Selection( + [ + ("manual_on_confirm", "Manually on Confirm"), + ("automatic_on_confirm", "Automatically on Confirm"), + ("manual_after_receipt", "Manually After Receipt"), + ("automatic_after_receipt", "Automatically After Receipt"), + ], + string="Delivery Action", + help="Define how the delivery action should be handled.", + default="manual_after_receipt", + ) + action_create_refund = fields.Selection( + [ + ("manual_on_confirm", "Manually on Confirm"), + ("automatic_on_confirm", "Automatically on Confirm"), + ("manual_after_receipt", "Manually After Receipt"), + ("automatic_after_receipt", "Automatically After Receipt"), + ("update_quantity", "Update Quantities"), + ], + string="Refund Action", + default="manual_after_receipt", + help="Define how the refund action should be handled.", + ) _sql_constraints = [ ("name_uniq", "unique (name)", "That operation name already exists !"), diff --git a/rma/static/description/index.html b/rma/static/description/index.html index 9975dc84f..635e3aee7 100644 --- a/rma/static/description/index.html +++ b/rma/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -530,9 +529,7 @@

Contributors

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

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" /> +