From 9500eaa46dfb4f93d312cce8d45f7b295a2caaea Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Mon, 1 Jul 2024 20:17:57 +0200 Subject: [PATCH] [IMP] mrp_sale_info: Materialize link between sale order lines and created MOs Improve test with 3 steps delivery route and use RecordCapturer --- mrp_sale_info/models/__init__.py | 2 + mrp_sale_info/models/sale_order_line.py | 57 +++++++++++++++ mrp_sale_info/models/stock_move.py | 16 +++++ mrp_sale_info/tests/test_mrp_sale_info.py | 85 +++++++++++++++++++++-- 4 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 mrp_sale_info/models/sale_order_line.py create mode 100644 mrp_sale_info/models/stock_move.py diff --git a/mrp_sale_info/models/__init__.py b/mrp_sale_info/models/__init__.py index ab9fed971..11cac02f0 100644 --- a/mrp_sale_info/models/__init__.py +++ b/mrp_sale_info/models/__init__.py @@ -2,4 +2,6 @@ from . import mrp_production from . import mrp_workorder +from . import sale_order_line +from . import stock_move from . import stock_rule diff --git a/mrp_sale_info/models/sale_order_line.py b/mrp_sale_info/models/sale_order_line.py new file mode 100644 index 000000000..6a83708bd --- /dev/null +++ b/mrp_sale_info/models/sale_order_line.py @@ -0,0 +1,57 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + created_production_ids = fields.Many2many( + "mrp.production", + compute="_compute_created_production_ids", + store=True, + ) + + @api.depends(lambda sol: sol._created_prod_move_deps()) + def _compute_created_production_ids(self): + for line in self: + delivery_moves = line.move_ids + mrp_production_ids = set() + for move in delivery_moves: + mrp_production_ids.update(move._get_orig_created_production_ids().ids) + # Consider MO Backorders for serial numbers + mrp_production_ids.update( + self.env["mrp.production"] + .browse(mrp_production_ids) + .mapped("procurement_group_id.mrp_production_ids") + .ids + ) + line.created_production_ids = [fields.Command.set(list(mrp_production_ids))] + + def _created_prod_move_deps(self): + fnames = [ + "move_ids", + "move_ids.created_production_id", + "move_ids.created_production_id.procurement_group_id.mrp_production_ids", + ] + # Allow customizing how many levels of recursive moves must be considered to + # compute the created_production_ids field on sale.order.line. + # This value must be at least equal to the number of: + # delivery steps + post manufacturing operations of the warehouse - 1. + # FIXME: Check if there's a way to recompute the dependency without having to + # restart Odoo. + try: + levels = int( + self.env["ir.config_parameter"] + .sudo() + .get_param("mrp_sale_info.compute.created_prod_ids_move_dep_levels", 3) + ) + except ValueError: + levels = 3 + for num in range(1, levels + 1): + fnames.append("move_ids.%screated_production_id" % ("move_orig_ids." * num)) + fnames.append( + "move_ids.%screated_production_id.procurement_group_id.mrp_production_ids" + % ("move_orig_ids." * num) + ) + return fnames diff --git a/mrp_sale_info/models/stock_move.py b/mrp_sale_info/models/stock_move.py new file mode 100644 index 000000000..62adde559 --- /dev/null +++ b/mrp_sale_info/models/stock_move.py @@ -0,0 +1,16 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _get_orig_created_production_ids(self): + self.ensure_one() + if self.created_production_id: + return self.created_production_id + mrp_production_ids = set() + for move in self.move_orig_ids: + mrp_production_ids.update(move._get_orig_created_production_ids().ids) + return self.env["mrp.production"].browse(mrp_production_ids) diff --git a/mrp_sale_info/tests/test_mrp_sale_info.py b/mrp_sale_info/tests/test_mrp_sale_info.py index aa6491df0..09303a5ef 100644 --- a/mrp_sale_info/tests/test_mrp_sale_info.py +++ b/mrp_sale_info/tests/test_mrp_sale_info.py @@ -1,13 +1,17 @@ # Copyright 2020 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tests import common +from odoo.tests import RecordCapturer, common class TestMrpSaleInfo(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + warehouse = cls.env.ref("stock.warehouse0") + warehouse.write( + {"delivery_steps": "pick_pack_ship", "manufacture_steps": "pbm_sam"} + ) route_manufacture_1 = cls.env.ref("mrp.route_warehouse0_manufacture") route_manufacture_2 = cls.env.ref("stock.route_warehouse0_mto") route_manufacture_2.active = True @@ -21,6 +25,26 @@ class TestMrpSaleInfo(common.TransactionCase): ], } ) + cls.product_kit = cls.env["product.product"].create( + { + "name": "Test kit", + "type": "product", + "route_ids": [ + (4, route_manufacture_1.id), + (4, route_manufacture_2.id), + ], + } + ) + cls.product_kit_comp = cls.env["product.product"].create( + { + "name": "Test kit comp", + "type": "product", + "route_ids": [ + (4, route_manufacture_1.id), + (4, route_manufacture_2.id), + ], + } + ) cls.bom = cls.env["mrp.bom"].create( { "product_tmpl_id": cls.product.product_tmpl_id.id, @@ -36,6 +60,28 @@ class TestMrpSaleInfo(common.TransactionCase): ], } ) + cls.bom_kit = cls.env["mrp.bom"].create( + { + "product_tmpl_id": cls.product_kit.product_tmpl_id.id, + "type": "phantom", + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + }, + ), + ( + 0, + 0, + { + "product_id": cls.product_kit_comp.id, + }, + ), + ], + } + ) cls.partner = cls.env["res.partner"].create({"name": "Test client"}) cls.sale_order = cls.env["sale.order"].create( { @@ -56,17 +102,42 @@ class TestMrpSaleInfo(common.TransactionCase): ) def test_mrp_sale_info(self): - prev_productions = self.env["mrp.production"].search([]) - self.sale_order.action_confirm() - production = self.env["mrp.production"].search([]) - prev_productions + with RecordCapturer(self.env["mrp.production"], []) as capture: + self.sale_order.action_confirm() + production = capture.records self.assertEqual(production.sale_id, self.sale_order) self.assertEqual(production.partner_id, self.partner) self.assertEqual(production.client_order_ref, self.sale_order.client_order_ref) + self.assertEqual(self.sale_order.order_line.created_production_ids, production) def test_mrp_workorder(self): - prev_workorders = self.env["mrp.workorder"].search([]) - self.sale_order.action_confirm() - workorder = self.env["mrp.workorder"].search([]) - prev_workorders + with RecordCapturer(self.env["mrp.workorder"], []) as capture: + self.sale_order.action_confirm() + workorder = capture.records self.assertEqual(workorder.sale_id, self.sale_order) self.assertEqual(workorder.partner_id, self.partner) self.assertEqual(workorder.client_order_ref, self.sale_order.client_order_ref) + + def test_kit(self): + self.sale_order.write( + { + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product_kit.id, + "product_uom_qty": 2, + "price_unit": 1, + }, + ) + ] + } + ) + with RecordCapturer(self.env["mrp.production"], []) as capture: + self.sale_order.action_confirm() + productions = capture.records + for order_line in self.sale_order.order_line: + for created_prod in order_line.created_production_ids: + self.assertIn(created_prod, productions) + self.assertEqual(created_prod.product_qty, order_line.product_uom_qty)