From 820af8c43fa03c87c9bb20c8a06e4ae1f421efa3 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Thu, 23 May 2024 17:58:02 +0200 Subject: [PATCH 1/2] [IMP] rma_sale : Always create rma line from sale line using stock move lines by default This allow to manage phantom bom products, by creating rma lines for the components instead of the kit if the option is activated. --- rma_sale/tests/test_rma_sale.py | 5 +- rma_sale/wizards/rma_add_sale.py | 112 ++++++++++++------ rma_sale_mrp/__init__.py | 1 + rma_sale_mrp/__manifest__.py | 2 +- rma_sale_mrp/models/__init__.py | 2 + rma_sale_mrp/models/res_company.py | 9 ++ rma_sale_mrp/models/res_config_settings.py | 14 +++ rma_sale_mrp/readme/DESCRIPTION.rst | 1 + rma_sale_mrp/tests/test_rma_mrp.py | 35 ++++++ .../views/res_config_settings_views.xml | 25 ++++ rma_sale_mrp/wizards/__init__.py | 1 + rma_sale_mrp/wizards/rma_add_sale.py | 16 +++ 12 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 rma_sale_mrp/models/res_company.py create mode 100644 rma_sale_mrp/models/res_config_settings.py create mode 100644 rma_sale_mrp/views/res_config_settings_views.xml create mode 100644 rma_sale_mrp/wizards/__init__.py create mode 100644 rma_sale_mrp/wizards/rma_add_sale.py diff --git a/rma_sale/tests/test_rma_sale.py b/rma_sale/tests/test_rma_sale.py index 8de84e6d..903c9609 100644 --- a/rma_sale/tests/test_rma_sale.py +++ b/rma_sale/tests/test_rma_sale.py @@ -65,7 +65,10 @@ class TestRmaSale(common.SingleTransactionCase): "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 1adcd1f5..7236c7d0 100644 --- a/rma_sale/wizards/rma_add_sale.py +++ b/rma_sale/wizards/rma_add_sale.py @@ -82,7 +82,7 @@ class RmaAddSale(models.TransientModel): "target": "new", } - def _prepare_rma_line_from_sale_order_line(self, line, lot=None): + def _prepare_rma_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 +121,26 @@ class RmaAddSale(models.TransientModel): 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 +165,84 @@ class RmaAddSale(models.TransientModel): 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( + 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( + 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 1cdbcf8b..166de0d2 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 50fd38ba..d0c02010 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 6ed3aa95..d203ea2b 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 00000000..fa238338 --- /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 00000000..40a6f7f5 --- /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 e69de29b..3372cbe9 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 cfdbef84..4b78b407 100644 --- a/rma_sale_mrp/tests/test_rma_mrp.py +++ b/rma_sale_mrp/tests/test_rma_mrp.py @@ -252,3 +252,38 @@ class TestRmaMrp(TransactionCase): 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 00000000..afcbba8b --- /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 00000000..b40d9dca --- /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 00000000..040a3961 --- /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) From 0bcd702a191d99d60f70272a0a43fe0f54404b51 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Wed, 31 Jul 2024 12:25:21 +0200 Subject: [PATCH 2/2] [REF] rename back method _prepare_rma_line to _prepare_rma_line_from_sale_order_line --- rma_sale/wizards/rma_add_sale.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rma_sale/wizards/rma_add_sale.py b/rma_sale/wizards/rma_add_sale.py index 7236c7d0..0aa18347 100644 --- a/rma_sale/wizards/rma_add_sale.py +++ b/rma_sale/wizards/rma_add_sale.py @@ -82,7 +82,9 @@ class RmaAddSale(models.TransientModel): "target": "new", } - def _prepare_rma_line(self, line, product, quantity, uom_id=False, 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 @@ -216,7 +218,7 @@ class RmaAddSale(models.TransientModel): line, existing_sale_line, lot=lot ): continue - data = self._prepare_rma_line( + data = self._prepare_rma_line_from_sale_order_line( line, product, qty, uom_id=uom.id, lot=lot ) rec = rma_line_obj.create(data) @@ -229,7 +231,7 @@ class RmaAddSale(models.TransientModel): 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( + data = self._prepare_rma_line_from_sale_order_line( line, line.product_id, line.product_uom_qty,