diff --git a/rma_sale/README.rst b/rma_sale/README.rst index 1d5b722f4..a7a3e4c3f 100644 --- a/rma_sale/README.rst +++ b/rma_sale/README.rst @@ -106,6 +106,8 @@ Contributors * Chafique Delli * Giovanni Serra - Ooops +* Souheil Bejaoui - ACSONE SA/NV +* Jacques-Etienne Baudoux - BCIM Maintainers ~~~~~~~~~~~ diff --git a/rma_sale/controllers/sale_portal.py b/rma_sale/controllers/sale_portal.py index 64e248113..b5cf00796 100644 --- a/rma_sale/controllers/sale_portal.py +++ b/rma_sale/controllers/sale_portal.py @@ -25,6 +25,7 @@ def request_rma(self, order_id, access_token=None, **post): except (AccessError, MissingError): return request.redirect("/my") order_obj = request.env["sale.order"] + order_line_obj = request.env["sale.order.line"] wizard_obj = request.env["sale.order.rma.wizard"].sudo() wizard_line_field_types = { f: d["type"] for f, d in wizard_obj.line_ids.fields_get().items() @@ -48,6 +49,9 @@ def request_rma(self, order_id, access_token=None, **post): # description values except ValueError: custom_vals.update({name: value}) + for vals in mapped_vals.values(): + sale_line = order_line_obj.browse(vals.get("sale_line_id")).sudo() + vals["allowed_quantity"] = sale_line.qty_delivered # If no operation is filled, no RMA will be created line_vals = [ (0, 0, vals) for vals in mapped_vals.values() if vals.get("operation_id") diff --git a/rma_sale/models/sale.py b/rma_sale/models/sale.py index 97b6209e9..4900bb600 100644 --- a/rma_sale/models/sale.py +++ b/rma_sale/models/sale.py @@ -31,6 +31,7 @@ def _prepare_rma_wizard_line_vals(self, data): return { "product_id": data["product"].id, "quantity": data["quantity"], + "allowed_quantity": data["quantity"], "sale_line_id": data["sale_line_id"].id, "uom_id": data["uom"].id, "picking_id": data["picking"] and data["picking"].id, diff --git a/rma_sale/readme/CONTRIBUTORS.rst b/rma_sale/readme/CONTRIBUTORS.rst index 75646b6d3..f89c48936 100644 --- a/rma_sale/readme/CONTRIBUTORS.rst +++ b/rma_sale/readme/CONTRIBUTORS.rst @@ -7,3 +7,5 @@ * Chafique Delli * Giovanni Serra - Ooops +* Souheil Bejaoui - ACSONE SA/NV +* Jacques-Etienne Baudoux - BCIM \ No newline at end of file diff --git a/rma_sale/static/description/index.html b/rma_sale/static/description/index.html index 30aa30340..76c2dafb0 100644 --- a/rma_sale/static/description/index.html +++ b/rma_sale/static/description/index.html @@ -455,6 +455,8 @@

Contributors

  • Chafique Delli <chafique.delli@akretion.com>
  • Giovanni Serra - Ooops <giovanni@ooops404.com>
  • +
  • Souheil Bejaoui - ACSONE SA/NV <souheil.bejaoui@acsone.eu>
  • +
  • Jacques-Etienne Baudoux - BCIM <je@bcim.be>
  • diff --git a/rma_sale/tests/__init__.py b/rma_sale/tests/__init__.py index 5318a7053..77bce454e 100644 --- a/rma_sale/tests/__init__.py +++ b/rma_sale/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_rma_sale from . import test_rma_sale_portal +from . import test_rma_sale_allowed_qty diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py index 532230d1d..3c38e8f1b 100644 --- a/rma_sale/tests/test_rma_sale.py +++ b/rma_sale/tests/test_rma_sale.py @@ -163,6 +163,7 @@ def test_create_rma_from_so_portal_user(self): "product_id": order.order_line.product_id.id, "sale_line_id": order.order_line.id, "quantity": order.order_line.product_uom_qty, + "allowed_quantity": order.order_line.qty_delivered, "uom_id": order.order_line.product_uom.id, "picking_id": order.picking_ids[0].id, "operation_id": operation.id, diff --git a/rma_sale/tests/test_rma_sale_allowed_qty.py b/rma_sale/tests/test_rma_sale_allowed_qty.py new file mode 100644 index 000000000..d926760ab --- /dev/null +++ b/rma_sale/tests/test_rma_sale_allowed_qty.py @@ -0,0 +1,110 @@ +# Copyright 2024 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestRmaSaleQuantityAllowed(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.loc_stock = cls.warehouse.lot_stock_id + cls.partner1 = cls.env["res.partner"].create({"name": "Partner"}) + cls.p1 = cls.env["product.product"].create( + {"name": "Unittest P1", "type": "product"} + ) + cls.so = cls.env["sale.order"].create( + { + "partner_id": cls.partner1.id, + "order_line": [ + Command.create( + { + "name": cls.p1.name, + "product_id": cls.p1.id, + "product_uom_qty": 5, + "price_unit": 50, + }, + ) + ], + } + ) + cls.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": cls.p1.id, + "inventory_quantity": 10, + "location_id": cls.loc_stock.id, + } + )._apply_inventory() + cls.so.action_confirm() + cls.picking = cls.so.picking_ids[0] + + def _get_rma_wizard(self): + action = self.so.action_create_rma() + return self.env[action.get("res_model")].browse(action.get("res_id")) + + def _deliver(self, qty): + self.picking.move_line_ids.qty_done = qty + self.picking._action_done() + self.assertEqual(self.picking.state, "done") + self.assertEqual(self.so.order_line.qty_delivered, qty) + + def test_1(self): + """ + Test rma wizard: + + - fully deliver the so + - open rma wizard + expected: + - qty proposed: 5 + - allowed qty 5 + - qty 0 if is_return_all = False + """ + self._deliver(5) + wizard = self._get_rma_wizard() + self.assertEqual(len(wizard.line_ids), 1) + self.assertEqual(wizard.line_ids.quantity, 5) + self.assertEqual(wizard.line_ids.allowed_quantity, 5) + wizard.is_return_all = False + self.assertEqual(wizard.line_ids.quantity, 0) + wizard.is_return_all = True + self.assertEqual(wizard.line_ids.quantity, 5) + + def test_2(self): + """ + Test rma wizard: + + - partially deliver the so + - open rma wizard + expected: + - qty proposed: 3 + - allowed qty 3 + - qty 0 if is_return_all = False + """ + self._deliver(3) + wizard = self._get_rma_wizard() + self.assertEqual(len(wizard.line_ids), 1) + self.assertEqual(wizard.line_ids.quantity, 3) + self.assertEqual(wizard.line_ids.allowed_quantity, 3) + wizard.is_return_all = False + self.assertEqual(wizard.line_ids.quantity, 0) + wizard.is_return_all = True + self.assertEqual(wizard.line_ids.quantity, 3) + + def test_3(self): + """ + Test rma wizard: + Try to return more than the allowed qty + """ + self._deliver(3) + wizard = self._get_rma_wizard() + self.assertEqual(len(wizard.line_ids), 1) + self.assertEqual(wizard.line_ids.quantity, 3) + self.assertEqual(wizard.line_ids.allowed_quantity, 3) + with self.assertRaises( + ValidationError, msg="You can't exceed the allowed quantity" + ): + wizard.line_ids.quantity = 5 + wizard.line_ids.quantity = 1 diff --git a/rma_sale/wizard/sale_order_rma_wizard.py b/rma_sale/wizard/sale_order_rma_wizard.py index 60146c84f..e03057e7e 100644 --- a/rma_sale/wizard/sale_order_rma_wizard.py +++ b/rma_sale/wizard/sale_order_rma_wizard.py @@ -1,8 +1,11 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda # Copyright 2022 Tecnativa - Víctor Martínez +# Copyright 2024 ACSONE SA/NV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import SUPERUSER_ID, _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare class SaleOrderRmaWizard(models.TransientModel): @@ -50,6 +53,7 @@ def _domain_location_id(self): custom_description = fields.Text( help="Values coming from portal RMA request form custom fields", ) + is_return_all = fields.Boolean(string="Return All?", default=True) def create_rma(self, from_portal=False): self.ensure_one() @@ -130,7 +134,11 @@ class SaleOrderLineRmaWizard(models.TransientModel): quantity = fields.Float( digits="Product Unit of Measure", required=True, + compute="_compute_quantity", + store=True, + readonly=False, ) + allowed_quantity = fields.Float(digits="Product Unit of Measure", readonly=True) uom_id = fields.Many2one( comodel_name="uom.uom", string="Unit of Measure", @@ -158,6 +166,14 @@ class SaleOrderLineRmaWizard(models.TransientModel): ) description = fields.Text() + @api.depends("wizard_id.is_return_all", "allowed_quantity") + def _compute_quantity(self): + for rec in self: + if not rec.wizard_id.is_return_all: + rec.quantity = 0 + else: + rec.quantity = rec.allowed_quantity + @api.depends("wizard_id.operation_id") def _compute_operation_id(self): for rec in self: @@ -200,6 +216,26 @@ def _compute_allowed_picking_ids(self): lambda x: x.state == "done" ) + @api.constrains("quantity", "allowed_quantity") + def _check_quantity(self): + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + for rec in self: + if ( + float_compare( + rec.quantity, rec.allowed_quantity, precision_digits=precision + ) + == 1 + ): + raise ValidationError( + _( + "You can't exceed the allowed quantity for returning product " + "%(product)s.", + product=rec.product_id.display_name, + ) + ) + def _prepare_rma_values(self): self.ensure_one() partner_shipping = ( diff --git a/rma_sale/wizard/sale_order_rma_wizard_views.xml b/rma_sale/wizard/sale_order_rma_wizard_views.xml index 24e0f0cb4..8bd1289bf 100644 --- a/rma_sale/wizard/sale_order_rma_wizard_views.xml +++ b/rma_sale/wizard/sale_order_rma_wizard_views.xml @@ -12,6 +12,7 @@
    + @@ -19,6 +20,7 @@ +