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