From acd3556463fa1fbc376b6c5252254110019753ff Mon Sep 17 00:00:00 2001
From: Adrien Widart <awt@odoo.com>
Date: Mon, 25 Oct 2021 10:19:53 +0000
Subject: [PATCH] [FIX] mrp_account, sale_mrp: check if BoM line is correct

Suppose an automated AVCO product category. When confirming an invoice,
if the associated product was a kit and if its BoM has changed during
the process, a traceback can occur.

To reproduce the issue:
(Need sale_management)
1. Create a product category PC:
    - Costing Method: AVCO
    - Inventory Valuation: Automated
2. Create 3 products P1, P2, P3:
    - Product Type: Storable
    - Category: PC
3. Update P3's quantity > 0
4. Create two bills of materials:
    - BOM01:
        - Product: P1
        - BoM Type: Kit
        - Components: 1 x P2
    - BOM02:
        - Product: P2
        - BoM Type: Kit
        - Components: 1 x P3
5. Create a sale order SO with 1 x P1
6. Confirm SO and process the delivery
7. Edit BOM02:
    - BoM Type: Manufacture
8. On SO, create the invoice INV
9. Confirm INV

Error: an Odoo Server Error is displayed with a traceback: "[...] in
_compute_average_price, bom_line_data = bom_lines[bom_line] [...]
KeyError: mrp.bom.line(19,)"

On step 7, when changing the BoM type, a new BoM line is created.
Therefore, in `_compute_average_price`, the BoM associated to the move
(i.e., `bom_line`) is not one of the lines in `bom_lines` (i.e., the new
BoM lines). There is already a check in case the line has been deleted,
but not if it has been changed.

OPW-2610685

closes odoo/odoo#78917

Signed-off-by: William Henrotin (whe) <whe@odoo.com>
---
 addons/mrp_account/models/product.py        |  2 +-
 addons/sale_mrp/tests/test_sale_mrp_flow.py | 50 +++++++++++++++++++++
 2 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py
index 82122b6de01c..8b975609bc8d 100644
--- a/addons/mrp_account/models/product.py
+++ b/addons/mrp_account/models/product.py
@@ -54,7 +54,7 @@ class ProductProduct(models.Model):
             if move.state == 'cancel':
                 continue
             bom_line = move.bom_line_id
-            if bom_line:
+            if bom_line in bom_lines:
                 bom_line_data = bom_lines[bom_line]
                 line_qty = bom_line.product_uom_id._compute_quantity(bom_line_data['qty'], bom_line.product_id.uom_id)
             else:
diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py
index 4c9bde734de9..f940912c3135 100644
--- a/addons/sale_mrp/tests/test_sale_mrp_flow.py
+++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py
@@ -1860,3 +1860,53 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
         self.assertNotEqual(qty_del_return_validated, 1.0, "The return was validated, therefore the delivery from client to"
                                                            " company was successful, and the client is left without his 1 product.")
         self.assertEqual(qty_del_return_validated, 0.0, "The return has processed, client doesn't have any quantity anymore")
+
+    def test_14_change_bom_type(self):
+        """ This test ensures that updating a Bom type during a flow does not lead to any error """
+        p1 = self._cls_create_product('Master', self.uom_unit)
+        p2 = self._cls_create_product('Component', self.uom_unit)
+        p3 = self.component_a
+        p1.categ_id.write({
+            'property_cost_method': 'average',
+            'property_valuation': 'real_time',
+        })
+        stock_location = self.company_data['default_warehouse'].lot_stock_id
+        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1)
+
+        self.env['mrp.bom'].create({
+            'product_tmpl_id': p1.product_tmpl_id.id,
+            'product_qty': 1.0,
+            'type': 'phantom',
+            'bom_line_ids': [(0, 0, {
+                'product_id': p2.id,
+                'product_qty': 1.0,
+            })]
+        })
+
+        p2_bom = self.env['mrp.bom'].create({
+            'product_tmpl_id': p2.product_tmpl_id.id,
+            'product_qty': 1.0,
+            'type': 'phantom',
+            'bom_line_ids': [(0, 0, {
+                'product_id': p3.id,
+                'product_qty': 1.0,
+            })]
+        })
+
+        so_form = Form(self.env['sale.order'])
+        so_form.partner_id = self.env['res.partner'].create({'name': 'Super Partner'})
+        with so_form.order_line.new() as so_line:
+            so_line.product_id = p1
+        so = so_form.save()
+        so.action_confirm()
+
+        wiz_act = so.picking_ids.button_validate()
+        wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+        wiz.process()
+
+        p2_bom.type = "normal"
+
+        so._create_invoices()
+        invoice = so.invoice_ids
+        invoice.action_post()
+        self.assertEqual(invoice.state, 'posted')
-- 
GitLab