From 0880d90ea20ed70bde58992a10d53397ee8ecd3b Mon Sep 17 00:00:00 2001 From: sbejaoui Date: Thu, 15 Aug 2024 13:21:00 +0200 Subject: [PATCH 1/7] [IMP] rma: configurable workflow --- rma/__manifest__.py | 1 + rma/models/rma.py | 137 +++++++++++++++-- rma/models/rma_operation.py | 31 ++++ rma/static/description/index.html | 11 +- rma/tests/__init__.py | 1 + rma/tests/test_rma_operation.py | 235 ++++++++++++++++++++++++++++++ rma/views/rma_operation.xml | 76 ++++++++++ rma/views/rma_views.xml | 20 ++- 8 files changed, 486 insertions(+), 26 deletions(-) create mode 100644 rma/tests/test_rma_operation.py create mode 100644 rma/views/rma_operation.xml 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..90112535a 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -317,6 +317,55 @@ 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" + ) + + @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 +443,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 +463,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 +486,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): @@ -726,20 +801,47 @@ 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_return( + fields.Datetime.now(), 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 +1478,15 @@ 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_return( + fields.Datetime.now(), 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..39f25d9ae 100644 --- a/rma/models/rma_operation.py +++ b/rma/models/rma_operation.py @@ -10,6 +10,37 @@ 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.", + ) + 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"), + ], + 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..c90f60381 --- /dev/null +++ b/rma/tests/test_rma_operation.py @@ -0,0 +1,235 @@ +# 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_return") + 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_return") + 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_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.action_confirm() + + 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) diff --git a/rma/views/rma_operation.xml b/rma/views/rma_operation.xml new file mode 100644 index 000000000..02de01896 --- /dev/null +++ b/rma/views/rma_operation.xml @@ -0,0 +1,76 @@ + + + + + + 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..e100dcac9 100644 --- a/rma/views/rma_views.xml +++ b/rma/views/rma_views.xml @@ -136,25 +136,32 @@ states="draft" class="btn-primary" /> +