From 9971fb02cf494c07c879883979166a66f1a62a17 Mon Sep 17 00:00:00 2001 From: clesgow Date: Fri, 22 Mar 2024 10:03:34 +0000 Subject: [PATCH] [FIX] {purchase_,}mrp: display routes for subcontracted boms 1. Fixes an issue where the parent product wasn't correctly set when computing the routes of a component, leading on components from subcontracted products displaying the wrong route (as it was trying to resupply the selected warehouse instead of the subcontracted location). 2. If a route is found when searching for subcontracting routes but doesn't lead to a way to resupply the stock (either buy buying or manufacturing something), then ignore the found rules and revert to the default of trying to resupply the stock location. This avoids issue when using reordering rules to resupply the subcontracted location instead, where the 'Buy' route would be hidden even if it was selected. closes odoo/odoo#159516 X-original-commit: c7b84a4a602f72c7a67144b7a81b38afe36e0eee Signed-off-by: Quentin Wolfs (quwo) Signed-off-by: Steve Van Essche --- addons/mrp/report/mrp_report_bom_structure.py | 7 +++ .../tests/test_purchase_subcontracting.py | 63 +++++++++++++++++++ .../report/mrp_report_bom_structure.py | 4 ++ 3 files changed, 74 insertions(+) diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index d7f4cd29256..8faa26110cc 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -573,6 +573,9 @@ class ReportBomStructure(models.AbstractModel): found_rules = [] if self._need_special_rules(product_info, parent_bom, parent_product): found_rules = self._find_special_rules(product, product_info, parent_bom, parent_product) + if found_rules and not self._is_resupply_rules(found_rules, bom): + # We only want to show the effective resupply (i.e. a form of manufacture or buy) + found_rules = [] if not found_rules: found_rules = product._get_rules_from_location(warehouse.lot_stock_id) if not found_rules: @@ -580,6 +583,10 @@ class ReportBomStructure(models.AbstractModel): rules_delay = sum(rule.delay for rule in found_rules) return self.with_context(parent_bom=parent_bom)._format_route_info(found_rules, rules_delay, warehouse, product, bom, quantity) + @api.model + def _is_resupply_rules(self, rules, bom): + return bom and any(rule.action == 'manufacture' for rule in rules) + @api.model def _need_special_rules(self, product_info, parent_bom=False, parent_product=False): return False diff --git a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py index f884281fe2a..332f3b862b8 100644 --- a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py +++ b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import Command from odoo.tests import Form from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon @@ -403,3 +404,65 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon): {'product_id': component.id, 'product_qty': 1.0}, {'product_id': component.id, 'product_qty': 1.0}, ]) + + def test_subcontracted_bom_routes(self): + """ + Take two BoM having those components. One being subcontracted and the other not. + - Compo RR : Buy & Reordering rule to resupply subcontractor. + - Compo DROP : Buy & Dropship subcontractor on order. + Check that depending on the context, the right route is shown on the report. + """ + route_buy = self.env.ref('purchase_stock.route_warehouse0_buy') + route_dropship = self.env['stock.route'].search([('name', '=', 'Dropship Subcontractor on Order')], limit=1) + warehouse = self.env['stock.warehouse'].search([], limit=1) + + compo_drop, compo_rr = self.env['product.product'].create([{ + 'name': name, + 'type': 'product', + 'seller_ids': [Command.create({'partner_id': self.subcontractor_partner1.parent_id.id})], + 'route_ids': [Command.set(routes)], + } for name, routes in [ + ('Compo DROP', [route_buy.id, route_dropship.id]), + ('Compo RR', [route_buy.id]), + ]]) + + route_resupply = self.env['stock.route'].search([('name', '=like', '%Resupply Subcontractor'), ('warehouse_ids', '=', warehouse.id)]) + route_resupply.product_selectable = True + self.env['stock.warehouse.orderpoint'].create({ + 'name': 'Resupply Subcontractor', + 'location_id': self.subcontractor_partner1.property_stock_subcontractor.id, + 'route_id': route_resupply.id, + 'product_id': compo_rr.id, + 'product_min_qty': 0, + 'product_max_qty': 0, + }) + compo_rr.route_ids = [Command.link(route_resupply.id)] + + bom_subcontract, bom_local = self.env['mrp.bom'].create([{ + 'product_tmpl_id': self.comp1.product_tmpl_id.id, + 'type': bom_type, + 'subcontractor_ids': partner_id, + 'bom_line_ids': [ + Command.create({'product_id': compo_drop.id, 'product_qty': 1}), + Command.create({'product_id': compo_rr.id, 'product_qty': 1}), + ] + } for bom_type, partner_id in [ + ('subcontract', [Command.link(self.subcontractor_partner1.id)]), + ('normal', False), + ]]) + # Need to add the subcontractor as Vendor to have the bom read as subcontracted. + self.comp1.write({'seller_ids': [Command.create({'partner_id': self.subcontractor_partner1.id})]}) + + report = self.env['report.mrp.report_bom_structure'].with_context(warehouse=warehouse.id)._get_report_data(bom_subcontract.id) + component_lines = report.get('lines', []).get('components', []) + self.assertEqual(component_lines[0]['product_id'], compo_drop.id) + self.assertEqual(component_lines[0]['route_name'], 'Dropship Subcontractor on Order') + self.assertEqual(component_lines[1]['product_id'], compo_rr.id) + self.assertEqual(component_lines[1]['route_name'], 'Buy', 'Despite the RR linked to it, it should still display the Buy route') + + report = self.env['report.mrp.report_bom_structure'].with_context(warehouse=warehouse.id)._get_report_data(bom_local.id) + component_lines = report.get('lines', []).get('components', []) + self.assertEqual(component_lines[0]['product_id'], compo_drop.id) + self.assertEqual(component_lines[0]['route_name'], 'Buy', 'Outside of the subcontracted context, it should try to resupply stock.') + self.assertEqual(component_lines[1]['product_id'], compo_rr.id) + self.assertEqual(component_lines[1]['route_name'], 'Buy') diff --git a/addons/purchase_mrp/report/mrp_report_bom_structure.py b/addons/purchase_mrp/report/mrp_report_bom_structure.py index a9a64c39329..8b1089161da 100644 --- a/addons/purchase_mrp/report/mrp_report_bom_structure.py +++ b/addons/purchase_mrp/report/mrp_report_bom_structure.py @@ -25,6 +25,10 @@ class ReportBomStructure(models.AbstractModel): } return res + @api.model + def _is_resupply_rules(self, rules, bom): + return super()._is_resupply_rules(rules, bom) or any(rule.action == 'buy' for rule in rules) + @api.model def _is_buy_route(self, rules, product, bom): return any(rule for rule in rules if rule.action == 'buy' and product.seller_ids)