From eb2f6cfde88cc96cd43ccbf56139a1718c91c464 Mon Sep 17 00:00:00 2001 From: clesgow Date: Thu, 11 Apr 2024 15:44:18 +0200 Subject: [PATCH] [FIX] purchase_mrp,stock: Avoid issues with backorders in Overview Steps to reproduce: - Set warehouse to two-steps reception - Create a BoM using a MTO (buy) component - Create a MO for 2 units of that BoM - Produce 1 and create a backorder - Open the Overview Issue: When there is a backorder (or if the MO is splitted), then when browsing the dests through `_rollup_move_dests()`, at some point it will branch into two differents path, related to each backorders. This will raise a traceback, as we only expected a single MO at that point. To avoid this, we made the `_rollup_move_dests()` method batchable, so we can fetch all moves linked to a manufacturing order in a single call, avoiding this issue. Note: For consistency sake, updated the `_rollup_move_origs()` method to be batchable as well, to avoid confusion when using one or the other. opw-3849252 closes odoo/odoo#161790 X-original-commit: d2830fccbe5dc733673d048303f573bb57a398a6 Signed-off-by: Arnold Moyaux (arm) Signed-off-by: Quentin Wolfs (quwo) --- .../report/mrp_report_mo_overview.py | 12 +++-- .../tests/test_purchase_mrp_flow.py | 53 +++++++++++++++++++ addons/stock/models/stock_move.py | 18 +++---- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/addons/purchase_mrp/report/mrp_report_mo_overview.py b/addons/purchase_mrp/report/mrp_report_mo_overview.py index fda4f1ffc2e..ee6d715151c 100644 --- a/addons/purchase_mrp/report/mrp_report_mo_overview.py +++ b/addons/purchase_mrp/report/mrp_report_mo_overview.py @@ -16,13 +16,15 @@ class ReportMoOverview(models.AbstractModel): for po_line in po_lines: line_qty = po_line.product_qty - for move in po_line.move_dest_ids: - linked_production = self.env['stock.move'].browse(move._rollup_move_dests()).raw_material_production_id - # Only create specific lines for moves directly linked to a manufacturing order - if not linked_production: + # Need to fetch every move connected to a manufacturing order from this PO line. This can happen when: + # - Multiple MOs are linked to a single PO line (e.g. Same MTO component for multiple MO). + # - A MO has a backorder / is splitted. + dest_moves = self.env['stock.move'].browse(po_line.move_dest_ids._rollup_move_dests()) + for move in dest_moves: + if not move.raw_material_production_id: continue prod_qty = min(line_qty, move.product_uom._compute_quantity(move.product_uom_qty, po_line.product_uom)) - res.append(self._format_extra_replenishment(po_line, prod_qty, linked_production.id)) + res.append(self._format_extra_replenishment(po_line, prod_qty, move.raw_material_production_id.id)) line_qty -= prod_qty if line_qty: res.append(self._format_extra_replenishment(po_line, line_qty)) diff --git a/addons/purchase_mrp/tests/test_purchase_mrp_flow.py b/addons/purchase_mrp/tests/test_purchase_mrp_flow.py index b803eebda5a..dffee80b3ad 100644 --- a/addons/purchase_mrp/tests/test_purchase_mrp_flow.py +++ b/addons/purchase_mrp/tests/test_purchase_mrp_flow.py @@ -982,3 +982,56 @@ class TestPurchaseMrpFlow(AccountTestInvoicingCommon): svl = po.picking_ids[0].move_ids.stock_valuation_layer_ids self.assertEqual(svl[0].unit_cost, 67.5) self.assertEqual(svl[1].unit_cost, 135000000) + + def test_mo_overview_mto_purchase_with_backorders(self): + self.warehouse.reception_steps = 'two_steps' + # Enable MTO route for Component + self.env.ref('stock.route_warehouse0_mto').active = True + route_buy = self.warehouse.buy_pull_id.route_id.id + route_mto = self.warehouse.mto_pull_id.route_id.id + self.component_a.write({ + 'seller_ids': [ + Command.create({'partner_id': self.partner_a.id}, + )], + 'route_ids': [ + Command.link(route_buy), + Command.link(route_mto), + ], + }) + + bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.component_b.product_tmpl_id.id, + 'product_qty': 1.0, + 'bom_line_ids': [ + Command.create({ + 'product_id': self.component_a.id, + 'product_qty': 2.0, + }), + ], + }) + with Form(self.env['mrp.production']) as prod_form: + prod_form.product_id = self.component_b + prod_form.bom_id = bom + prod_form.product_qty = 3 + production = prod_form.save() + production.action_confirm() + self.assertEqual(production.purchase_order_count, 1) + purchase = production.procurement_group_id.stock_move_ids.created_purchase_line_ids.order_id + self.assertEqual(len(purchase), 1) + + with Form(production) as prod_form: + prod_form.qty_producing = 1 + production = prod_form.save() + backorder_action = production.button_mark_done() + backorder_wizard = Form(self.env['mrp.production.backorder'].with_context(**backorder_action['context'])) + backorder_wizard.save().action_backorder() + + backorder = production.procurement_group_id.mrp_production_ids - production + self.assertEqual(len(backorder), 1) + self.assertEqual(backorder.product_qty, 2) + report_values = self.env['report.mrp.report_mo_overview']._get_report_data(backorder.id) + self.assertEqual(report_values['summary']['quantity'], backorder.product_qty) + self.assertEqual(report_values['components'][0]['summary']['quantity'], 4) + replenishments = report_values['components'][0]['replenishments'] + self.assertEqual(len(replenishments), 1) + self.assertEqual(replenishments[0]['summary']['name'], purchase.name) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index b7a6118c983..54e8e74639c 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -2157,25 +2157,23 @@ Please change the quantity done or the rounding precision of your unit of measur moves_to_reserve._action_assign() def _rollup_move_dests(self, seen=False): - self.ensure_one() if not seen: seen = OrderedSet() - if self.id in seen: + unseen = OrderedSet(self.ids) - seen + if not unseen: return seen - seen.add(self.id) - for dst in self.move_dest_ids: - dst._rollup_move_dests(seen) + seen.update(unseen) + self.filtered(lambda m: m.id in unseen).move_dest_ids._rollup_move_dests(seen) return seen def _rollup_move_origs(self, seen=False): - self.ensure_one() if not seen: seen = OrderedSet() - if self.id in seen: + unseen = OrderedSet(self.ids) - seen + if not unseen: return seen - seen.add(self.id) - for org in self.move_orig_ids: - org._rollup_move_origs(seen) + seen.update(unseen) + self.filtered(lambda m: m.id in unseen).move_orig_ids._rollup_move_origs(seen) return seen def _get_forecast_availability_outgoing(self, warehouse):