From 08f7420cb4f5e5fa990f4f9aaaf0a9c3b99402be Mon Sep 17 00:00:00 2001
From: "Touati Djamel (otd)" <otd@odoo.com>
Date: Wed, 22 Sep 2021 09:49:54 +0000
Subject: [PATCH] [FIX] purchase_stock, purchase_mrp: adjust the qty of
 purchased kit correctly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Steps to reproduce the bug:
- Create a BOM kit for “product K”  with:
    - 2 * “product A”
    - 1 * “product B”
- Create a PO for 1 unit of “product K” > confirm
- A receipt delivery with 2 units of “product A” and 1 unit of B will be created
- Modify the ordered Qty to 2 units of “Product K”

Problem:
The receipt delivery will not be updated correctly (4 units of product A and 3 of product B)
because the `"_prepare_stock_moves"` function computed the previous quantity wrong based on the moves quantities
since the moves are for products A and B, not product F.(do not take into account the products in kit)

Solution:
For kit products, do not calculate from the `"stock.move"`, calculate the difference between the quantity before and after the change

opw-2645719

closes odoo/odoo#76965

Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
---
 addons/purchase_mrp/models/purchase_mrp.py    | 11 +++++
 .../tests/test_purchase_mrp_flow.py           | 45 +++++++++++++++++++
 addons/purchase_stock/models/purchase.py      | 16 +++++--
 3 files changed, 68 insertions(+), 4 deletions(-)

diff --git a/addons/purchase_mrp/models/purchase_mrp.py b/addons/purchase_mrp/models/purchase_mrp.py
index 50c30727c6ec..7facf43defdf 100644
--- a/addons/purchase_mrp/models/purchase_mrp.py
+++ b/addons/purchase_mrp/models/purchase_mrp.py
@@ -39,6 +39,17 @@ class PurchaseOrderLine(models.Model):
     def _get_upstream_documents_and_responsibles(self, visited):
         return [(self.order_id, self.order_id.user_id, visited)]
 
+    def _get_qty_procurement(self):
+        self.ensure_one()
+        # Specific case when we change the qty on a PO for a kit product.
+        # We don't try to be too smart and keep a simple approach: we compare the quantity before
+        # and after update, and return the difference. We don't take into account what was already
+        # sent, or any other exceptional case.
+        bom = self.env['mrp.bom']._bom_find(product=self.product_id)
+        if bom and bom.type == 'phantom' and 'previous_product_qty' in self.env.context:
+            return self.env.context['previous_product_qty'].get(self.id, 0.0)
+        return super()._get_qty_procurement()
+    
 class StockMove(models.Model):
     _inherit = 'stock.move'
 
diff --git a/addons/purchase_mrp/tests/test_purchase_mrp_flow.py b/addons/purchase_mrp/tests/test_purchase_mrp_flow.py
index 972d1bf79056..f8479c7c8d98 100644
--- a/addons/purchase_mrp/tests/test_purchase_mrp_flow.py
+++ b/addons/purchase_mrp/tests/test_purchase_mrp_flow.py
@@ -94,3 +94,48 @@ class TestPurchaseMrpFlow(common.TransactionCase):
         aml = self.invoice.move_id.line_ids
         aml_input = aml.filtered(lambda l: l.account_id.id == self.account_input.id)
         self.assertEqual(aml_input.debit, 180, "Cost of Good Sold entry missing or mismatching")
+
+    def test_01_purchase_mrp_kit_qty_change(self):
+        self.partner = self.env.ref('base.res_partner_1')
+        self.categ_unit = self.env.ref('uom.product_uom_categ_unit')
+        self.uom_unit = self.env['uom.uom'].search([('category_id', '=', self.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
+        Product = self.env['product.product']
+        self.kit_product = Product.create({
+                'name': 'product K',
+                'type': 'consu',
+                'uom_id': self.uom_unit.id,
+                })
+        self.component1 = Product.create({
+                'name': 'Component 1',
+                'type': 'product',})
+        self.component2 = Product.create({
+                'name': 'Component 2',
+                'type': 'product',})
+        self.bom = self.env['mrp.bom'].create({
+        'product_tmpl_id': self.kit_product.product_tmpl_id.id,
+        'product_qty': 1.0,
+        'type': 'phantom'})
+        BomLine = self.env['mrp.bom.line']
+        BomLine.create({
+                'product_id': self.component1.id,
+                'product_qty': 2.0,
+                'bom_id': self.bom.id})
+        BomLine.create({
+                'product_id': self.component2.id,
+                'product_qty': 1.0,
+                'bom_id': self.bom.id})
+        # Create a PO with one unit of the kit product
+        self.po = self.env['purchase.order'].create({
+            'partner_id': self.partner.id,
+            'order_line': [(0, 0, {'name': self.kit_product.name, 'product_id': self.kit_product.id, 'product_qty': 1, 'product_uom': self.kit_product.uom_id.id, 'price_unit': 60.0, 'date_planned': fields.Datetime.now()})],
+        })
+        # Validate the PO
+        self.po.button_confirm()
+        # Check the component qty in the created picking
+        self.assertEqual(self.po.picking_ids.move_ids_without_package[0].product_uom_qty,2, "The quantity of components must be created according to the BOM")
+        self.assertEqual(self.po.picking_ids.move_ids_without_package[1].product_uom_qty,1, "The quantity of components must be created according to the BOM")
+        
+        # Update the quantity of the kit in the PO
+        self.po.order_line[0].product_qty = 2
+        self.assertEqual(self.po.picking_ids.move_ids_without_package[0].product_uom_qty,4, "The amount of the kit components must be updated when changing the quantity of the kit.")
+        self.assertEqual(self.po.picking_ids.move_ids_without_package[1].product_uom_qty,2, "The amount of the kit components must be updated when changing the quantity of the kit.")
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index 5e0f1d1a34c1..e9316598d35a 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -231,6 +231,8 @@ class PurchaseOrderLine(models.Model):
 
     @api.multi
     def write(self, values):
+        lines = self.filtered(lambda l: l.order_id.state == 'purchase')
+        previous_product_qty = {line.id: line.product_uom_qty for line in lines}
         result = super(PurchaseOrderLine, self).write(values)
         # Update expected date of corresponding moves
         if 'date_planned' in values:
@@ -238,7 +240,7 @@ class PurchaseOrderLine(models.Model):
                 ('purchase_line_id', 'in', self.ids), ('state', '!=', 'done')
             ]).write({'date_expected': values['date_planned']})
         if 'product_qty' in values:
-            self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
+            lines.with_context(previous_product_qty=previous_product_qty)._create_or_update_picking()
         return result
 
     # --------------------------------------------------
@@ -304,10 +306,9 @@ class PurchaseOrderLine(models.Model):
         res = []
         if self.product_id.type not in ['product', 'consu']:
             return res
-        qty = 0.0
         price_unit = self._get_stock_move_price_unit()
-        for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
-            qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
+        qty = self._get_qty_procurement()
+
         template = {
             # truncate to 2000 to avoid triggering index limit error
             # TODO: remove index in master?
@@ -346,6 +347,13 @@ class PurchaseOrderLine(models.Model):
             res.append(template)
         return res
 
+    def _get_qty_procurement(self):
+        self.ensure_one()
+        qty = 0.0
+        for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
+            qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
+        return qty
+    
     @api.multi
     def _create_stock_moves(self, picking):
         values = []
-- 
GitLab