From a0c08751263f904d1040c994bec12b1eed9fbc55 Mon Sep 17 00:00:00 2001 From: "lase@odoo.com" Date: Fri, 9 Aug 2024 10:49:09 +0200 Subject: [PATCH] [FIX] stock: consider mto procure method in stock forecast Steps to reproduce: - Enable Multi-Step Routes in the settings - Go to Inventory > Configuration > Warehouse Management > Routes - Unarchive MTO - Create a storable product: - Routes: MTO, manufacture - With a bom - On hand qty: 10 - Create an SO for 15 units and validate - Go back to your product form > forecasted Report > Even thought 15 units are made to order 10 units are reported to be > taken from stock to fulfill the SO Cause of the issue: The lines appearing on the reported are computed by the `_get_report_lines` method. During this call, the quantity taken from stock is computed by comparing the demand of the out moves with the quantity currently in stock: https://github.com/odoo/odoo/blob/1fdccad664cad37a69dc960444aa0c318d1bf464/addons/stock/report/report_stock_forecasted.py#L218-L223 This is incorrect because the quantity taken from stock is null if the procure_method of the out move is make_to_stock. Note: The issue is not reproducible since 17.0 since the _get_report_lines has been refactored during the stockpocalypse. opw-4010785 closes odoo/odoo#176273 Signed-off-by: William Henrotin (whe) --- addons/sale_mrp/tests/test_sale_mrp_report.py | 58 +++++++++++++++++++ .../stock/report/report_stock_forecasted.py | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/addons/sale_mrp/tests/test_sale_mrp_report.py b/addons/sale_mrp/tests/test_sale_mrp_report.py index bcc6d7b9faabc..3f9969e9bc27f 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_report.py +++ b/addons/sale_mrp/tests/test_sale_mrp_report.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.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import common, Form from odoo.tools import html2plaintext @@ -70,3 +71,60 @@ def test_deliver_and_invoice_tracked_components(self): html = report._render_qweb_html(invoice.ids)[0] text = html2plaintext(html) self.assertRegex(text, r'Product By Lot\n1.00\nUnits\nLOT0001', "There should be a line that specifies 1 x LOT0001") + + def test_report_forecast_for_mto_procure_method(self): + """ + Check that mto moves are not reported as taking from stock in the forecast report + """ + mto_route = self.env.ref('stock.route_warehouse0_mto') + mto_route.active = True + manufacturing_route = self.env.ref('mrp.route_warehouse0_manufacture') + product = self.env['product.product'].create({ + 'name': 'SuperProduct', + 'type': 'product', + 'route_ids': [Command.set((mto_route + manufacturing_route).ids)] + }) + warehouse = self.warehouse + # make 2 so: so_1 can be fulfilled and so_2 requires a replenishment + self.env['stock.quant']._update_available_quantity(product, warehouse.lot_stock_id, 10.0) + so_1, so_2 = self.env['sale.order'].create([ + { + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 8.0, + 'product_uom': product.uom_id.id, + 'price_unit': product.list_price, + })] + }, + { + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 7.0, + 'product_uom': product.uom_id.id, + 'price_unit': product.list_price, + })] + }, + + ]) + (so_1 | so_2).action_confirm() + report_lines = self.env['report.stock.report_product_product_replenishment'].with_context(warehouse=warehouse.id)._get_report_values(docids=product.ids)['docs']['lines'] + self.assertEqual(len(report_lines), 3) + so_1_line = next(filter(lambda line: line.get('document_out') == so_1, report_lines)) + self.assertEqual( + [so_1_line['quantity'], so_1_line['move_out'], so_1_line['replenishment_filled']], + [8.0, so_1.picking_ids.move_lines, True] + ) + so_2_line = next(filter(lambda line: line.get('document_out') == so_2, report_lines)) + self.assertEqual( + [so_2_line['quantity'], so_2_line['move_out'], so_2_line['replenishment_filled']], + [7.0, so_2.picking_ids.move_lines, False] + ) + quant_line = next(filter(lambda line: not line.get('document_out'), report_lines)) + self.assertEqual( + [quant_line['document_out'], quant_line['quantity'], quant_line['replenishment_filled']], + [False, 2.0, True] + ) diff --git a/addons/stock/report/report_stock_forecasted.py b/addons/stock/report/report_stock_forecasted.py index 581123036a538..d1e1c81d5895b 100644 --- a/addons/stock/report/report_stock_forecasted.py +++ b/addons/stock/report/report_stock_forecasted.py @@ -220,7 +220,7 @@ def _reconcile_out_with_ins(lines, out, ins, demand, product_rounding, only_matc if float_is_zero(demand, precision_rounding=product_rounding): continue current = currents[product.id] - taken_from_stock = min(demand, current) + taken_from_stock = min(demand, current) if out.procure_method != 'make_to_order' else 0 if not float_is_zero(taken_from_stock, precision_rounding=product_rounding): currents[product.id] -= taken_from_stock demand -= taken_from_stock