diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py index 8de84e6df..903c96092 100644 --- a/rma_sale/tests/test_rma_sale.py +++ b/rma_sale/tests/test_rma_sale.py @@ -65,7 +65,10 @@ def setUpClass(cls): "pricelist_id": cls.env.ref("product.list0").id, } ) - + cls.so.action_confirm() + for move in cls.so.picking_ids.move_ids: + move.write({"quantity_done": move.product_uom_qty}) + cls.so.picking_ids._action_done() # Create RMA group and operation: cls.rma_group = cls.rma_obj.create({"partner_id": customer1.id}) cls.operation_1 = cls.rma_op_obj.create( diff --git a/rma_sale/wizards/rma_add_sale.py b/rma_sale/wizards/rma_add_sale.py index 1adcd1f57..0aa183473 100644 --- a/rma_sale/wizards/rma_add_sale.py +++ b/rma_sale/wizards/rma_add_sale.py @@ -82,7 +82,9 @@ def select_all(self): "target": "new", } - def _prepare_rma_line_from_sale_order_line(self, line, lot=None): + def _prepare_rma_line_from_sale_order_line( + self, line, product, quantity, uom_id=False, lot=None + ): operation = self.rma_id.operation_default_id if not operation: operation = line.product_id.rma_customer_operation_id @@ -121,33 +123,26 @@ def _prepare_rma_line_from_sale_order_line(self, line, lot=None): or operation.in_warehouse_id.lot_rma_id or warehouse.lot_rma_id ) - product_qty = line.product_uom_qty - if line.product_id.tracking == "serial": - product_qty = 1 - elif line.product_id.tracking == "lot": - product_qty = sum( - line.mapped("move_ids.move_line_ids") - .filtered(lambda x: x.lot_id.id == lot.id) - .mapped("qty_done") - ) data = { "partner_id": self.partner_id.id, "description": self.rma_id.description, "sale_line_id": line.id, - "product_id": line.product_id.id, + "product_id": product.id, "lot_id": lot and lot.id or False, "origin": line.order_id.name, - "uom_id": line.product_uom.id, + "uom_id": uom_id or product.uom_id.id, "operation_id": operation.id, - "product_qty": product_qty, + "product_qty": quantity, "delivery_address_id": self.sale_id.partner_shipping_id.id, "invoice_address_id": self.sale_id.partner_invoice_id.id, - "price_unit": line.currency_id._convert( + "price_unit": line.product_id == product + and line.currency_id._convert( line.price_unit, line.currency_id, line.company_id, line.order_id.date_order, - ), + ) + or product.lst_price, "rma_id": self.rma_id.id, "in_route_id": operation.in_route_id.id or route.id, "out_route_id": operation.out_route_id.id or route.id, @@ -172,35 +167,84 @@ def _get_existing_sale_lines(self): existing_sale_lines.append(rma_line.sale_line_id) return existing_sale_lines + def _should_create_rma_line(self, line, existing_sale_line, lot=False): + if not lot and line in existing_sale_line: + return False + if lot and ( + lot.id not in self.lot_ids.ids + or lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids + ): + return False + return True + + def _create_from_move_line(self, line): + return True + + def _get_lot_quantity_from_move_lines(self, sale_line): + outgoing_lines = self.env["stock.move.line"] + incoming_lines = self.env["stock.move.line"] + sent_moves = sale_line.move_ids.filtered( + lambda m: m.state == "done" and not m.scrapped + ) + for move in sent_moves: + if move.location_dest_id.usage == "customer" and ( + not move.origin_returned_move_id + or (move.origin_returned_move_id and move.to_refund) + ): + outgoing_lines |= move.move_line_ids + elif move.location_dest_id.usage != "customer" and move.to_refund: + incoming_lines |= move.move_line_ids + sent_product_data = {} + for line in outgoing_lines: + key = (line.product_id, line.product_uom_id, line.lot_id) + if key not in sent_product_data: + sent_product_data[key] = 0.0 + sent_product_data[key] += line.qty_done + for line in incoming_lines: + key = (line.product_id, line.product_uom_id, line.lot_id) + if key not in sent_product_data: + sent_product_data[key] = 0.0 + sent_product_data[key] -= line.qty_done + return sent_product_data + def add_lines(self): rma_line_obj = self.env["rma.order.line"] - existing_sale_lines = self._get_existing_sale_lines() + existing_sale_line = self._get_existing_sale_lines() for line in self.sale_line_ids: - tracking_move = line.product_id.tracking in ("serial", "lot") - # Load a PO line only once - if line not in existing_sale_lines or tracking_move: - if not tracking_move: - data = self._prepare_rma_line_from_sale_order_line(line) + if self._create_from_move_line(line): + sent_produt_data = self._get_lot_quantity_from_move_lines(line) + for (product, uom, lot), qty in sent_produt_data.items(): + if not self._should_create_rma_line( + line, existing_sale_line, lot=lot + ): + continue + data = self._prepare_rma_line_from_sale_order_line( + line, product, qty, uom_id=uom.id, lot=lot + ) rec = rma_line_obj.create(data) # Ensure that configuration on the operation is applied # TODO MIG: in v16 the usage of such onchange can be removed in # favor of (pre)computed stored editable fields for all policies # and configuration in the RMA operation. rec._onchange_operation_id() - else: - for lot in line.mapped("move_ids.move_line_ids.lot_id").filtered( - lambda x: x.id in self.lot_ids.ids - ): - if lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids: - continue - data = self._prepare_rma_line_from_sale_order_line(line, lot) - rec = rma_line_obj.create(data) - # Ensure that configuration on the operation is applied - # TODO MIG: in v16 the usage of such onchange can be removed in - # favor of (pre)computed stored editable fields for all policies - # and configuration in the RMA operation. - rec._onchange_operation_id() - rec.price_unit = rec._get_price_unit() + else: + if not self._should_create_rma_line(line, existing_sale_line): + continue + # we can't have lot management based on sale order line + data = self._prepare_rma_line_from_sale_order_line( + line, + line.product_id, + line.product_uom_qty, + uom_id=line.product_uom.id, + lot=False, + ) + rec = rma_line_obj.create(data) + # Ensure that configuration on the operation is applied + # TODO MIG: in v16 the usage of such onchange can be removed in + # favor of (pre)computed stored editable fields for all policies + # and configuration in the RMA operation. + rec._onchange_operation_id() + rec.price_unit = rec._get_price_unit() rma = self.rma_id data_rma = self._get_rma_data() rma.write(data_rma) diff --git a/rma_sale_mrp/__init__.py b/rma_sale_mrp/__init__.py index 1cdbcf8be..166de0d20 100644 --- a/rma_sale_mrp/__init__.py +++ b/rma_sale_mrp/__init__.py @@ -1,3 +1,4 @@ # Copyright 2023 ForgeFlow S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) from . import models +from . import wizards diff --git a/rma_sale_mrp/__manifest__.py b/rma_sale_mrp/__manifest__.py index 50fd38ba5..d0c020107 100644 --- a/rma_sale_mrp/__manifest__.py +++ b/rma_sale_mrp/__manifest__.py @@ -10,6 +10,6 @@ "author": "ForgeFlow", "website": "https://github.com/ForgeFlow/stock-rma", "depends": ["rma_sale", "sale_mrp"], - "data": [], + "data": ["views/res_config_settings_views.xml"], "installable": True, } diff --git a/rma_sale_mrp/models/__init__.py b/rma_sale_mrp/models/__init__.py index 6ed3aa953..d203ea2b2 100644 --- a/rma_sale_mrp/models/__init__.py +++ b/rma_sale_mrp/models/__init__.py @@ -1,3 +1,5 @@ # Copyright 2023 ForgeFlow S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) from . import rma_order_line +from . import res_company +from . import res_config_settings diff --git a/rma_sale_mrp/models/res_company.py b/rma_sale_mrp/models/res_company.py new file mode 100644 index 000000000..fa2383388 --- /dev/null +++ b/rma_sale_mrp/models/res_company.py @@ -0,0 +1,9 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + rma_add_component_from_sale = fields.Boolean() diff --git a/rma_sale_mrp/models/res_config_settings.py b/rma_sale_mrp/models/res_config_settings.py new file mode 100644 index 000000000..40a6f7f54 --- /dev/null +++ b/rma_sale_mrp/models/res_config_settings.py @@ -0,0 +1,14 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + rma_add_component_from_sale = fields.Boolean( + related="company_id.rma_add_component_from_sale", + readonly=False, + help="If active, when creating a rma from a sale order, in case the product " + "is a kit, the delivered components will be added instead of the kit.", + ) diff --git a/rma_sale_mrp/readme/DESCRIPTION.rst b/rma_sale_mrp/readme/DESCRIPTION.rst index e69de29bb..3372cbe93 100644 --- a/rma_sale_mrp/readme/DESCRIPTION.rst +++ b/rma_sale_mrp/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Add an parameter at company level to choose if rma lines are created for a kit or for the components, in the case the rma lines are created from a sale order. diff --git a/rma_sale_mrp/tests/test_rma_mrp.py b/rma_sale_mrp/tests/test_rma_mrp.py index cfdbef84f..4b78b407f 100644 --- a/rma_sale_mrp/tests/test_rma_mrp.py +++ b/rma_sale_mrp/tests/test_rma_mrp.py @@ -252,3 +252,38 @@ def test_01_kit_return_with_diff_prices(self): self.assertEqual( 150.0, sum(component_2_sm.mapped("stock_valuation_layer_ids.value")) ) + + def test_02_add_kit_from_sale(self): + order_01 = self._make_sale_order(self.kit_product, 2, 30.0) + self._do_picking(order_01.picking_ids, 2.0) + rma = self.env["rma.order"].create({"partner_id": self.customer.id}) + add_sale = ( + self.env["rma_add_sale"] + .with_context(active_model="rma.order", active_ids=rma.ids) + .create( + { + "sale_id": order_01.id, + "sale_line_ids": [(6, 0, order_01.order_line.ids)], + } + ) + ) + add_sale.add_lines() + # component config is not set, we should create a rma line for the kit. + self.assertEqual(len(rma.rma_line_ids), 1) + self.assertEqual(rma.rma_line_ids.product_id, self.kit_product) + self.assertEqual(rma.rma_line_ids.product_qty, 2.0) + + # test with component config now + rma.rma_line_ids.unlink() + order_01.company_id.write({"rma_add_component_from_sale": True}) + add_sale.add_lines() + self.assertEqual(len(rma.rma_line_ids), 2) + line_component_1 = rma.rma_line_ids.filtered( + lambda line: line.product_id == self.component_product_1 + ) + line_component_2 = rma.rma_line_ids.filtered( + lambda line: line.product_id == self.component_product_2 + ) + self.assertTrue(line_component_1) + self.assertEqual(line_component_1.product_qty, 2.0) + self.assertTrue(line_component_2) diff --git a/rma_sale_mrp/views/res_config_settings_views.xml b/rma_sale_mrp/views/res_config_settings_views.xml new file mode 100644 index 000000000..afcbba8bf --- /dev/null +++ b/rma_sale_mrp/views/res_config_settings_views.xml @@ -0,0 +1,25 @@ + + + + + res.config.settings + + + + +
+
+ +
+
+
+
+
+
+
+ +
diff --git a/rma_sale_mrp/wizards/__init__.py b/rma_sale_mrp/wizards/__init__.py new file mode 100644 index 000000000..b40d9dcae --- /dev/null +++ b/rma_sale_mrp/wizards/__init__.py @@ -0,0 +1 @@ +from . import rma_add_sale diff --git a/rma_sale_mrp/wizards/rma_add_sale.py b/rma_sale_mrp/wizards/rma_add_sale.py new file mode 100644 index 000000000..040a3961e --- /dev/null +++ b/rma_sale_mrp/wizards/rma_add_sale.py @@ -0,0 +1,16 @@ +# Copyright 2020 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import models + + +class RmaAddSale(models.TransientModel): + _inherit = "rma_add_sale" + + def _create_from_move_line(self, line): + phantom_bom = line.move_ids.bom_line_id.bom_id.filtered( + lambda bom: bom.type == "phantom" + ) + if phantom_bom and not line.company_id.rma_add_component_from_sale: + return False + return super()._create_from_move_line(line)