From ee66e431337c7198ae68287126a3f044048cf8be Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Fri, 17 Dec 2021 14:05:41 +0000 Subject: [PATCH] [FIX] mrp_account: ignore returned SM in price computations When returning a kit, the margin of the related SO decreases To reproduce the issue: (Need sale_management) 1. In Settings, enable "Margins" 2. Create a Product Category PC: - Costing Method: FIFO 3. Create two products P_kit, P_compo: - Both: - Type: Storable - Category: PC - P_compo: - Cost: 10 4. Update P_compo quantity: 1 5. Create a BoM: - Product: P_kit - Type: Kit - Components: 1 x P_compo 6. Update P_kit's cost 7. Create and confirm a SO with 1 x P_kit, unit price = $100 - Note that the margin is correct: $90 8. Process the related picking 9. Create a return R 10. Go back to the SO Error: the margin is now $80 When computing the cost of the kit, the move linked to R are considered. Therefore, the cost becomes $20 and thus the margin becomes $80 OPW-2679473 closes odoo/odoo#83008 X-original-commit: 2f34669f9dc3c9abfcbb13198f90f37405ffc258 Signed-off-by: Arnold Moyaux Signed-off-by: Adrien Widart --- addons/mrp_account/models/product.py | 21 ++++------ addons/sale_mrp/tests/test_sale_mrp_flow.py | 46 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py index b2a8a4ee082..8aa27a0fae4 100644 --- a/addons/mrp_account/models/product.py +++ b/addons/mrp_account/models/product.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models -from odoo.tools import float_round +from odoo.tools import float_round, groupby class ProductTemplate(models.Model): @@ -53,20 +53,17 @@ class ProductProduct(models.Model): bom = self.env['mrp.bom']._bom_find(self, company_id=stock_moves.company_id.id, bom_type='phantom')[self] if not bom: return super()._compute_average_price(qty_invoiced, qty_to_invoice, stock_moves, is_returned=is_returned) + value = 0 dummy, bom_lines = bom.explode(self, 1) bom_lines = {line: data for line, data in bom_lines} - value = 0 - for move in stock_moves: - if move.state == 'cancel': + for bom_line, moves_list in groupby(stock_moves.filtered(lambda sm: sm.state != 'cancel'), lambda sm: sm.bom_line_id): + if bom_line not in bom_lines: + for move in moves_list: + value += move.product_qty * move.product_id._compute_average_price(qty_invoiced * move.product_qty, qty_to_invoice * move.product_qty, move, is_returned=is_returned) continue - bom_line = move.bom_line_id - 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: - # bom was altered (i.e. bom line removed) after being used - line_qty = move.product_qty - value += line_qty * move.product_id._compute_average_price(qty_invoiced * line_qty, qty_to_invoice * line_qty, move, is_returned=is_returned) + line_qty = bom_line.product_uom_id._compute_quantity(bom_line.product_qty, bom_line.product_id.uom_id) + moves = self.env['stock.move'].concat(*moves_list) + value += line_qty * bom_line.product_id._compute_average_price(qty_invoiced * line_qty, qty_to_invoice * line_qty, moves, is_returned=is_returned) return value def _compute_bom_price(self, bom, boms_to_recompute=False, byproduct_bom=False): diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 17781849af9..ce7e7407981 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -2041,3 +2041,49 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon): cogs_aml = amls.filtered(lambda aml: aml.account_id == expense_account) self.assertEqual(cogs_aml.debit, 0) self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component') + + def test_kit_margin_and_return_picking(self): + """ This test ensure that, when returning the components of a sold kit, the + sale order line cost does not change""" + kit = self._cls_create_product('Super Kit', self.uom_unit) + (kit + self.component_a).categ_id.property_cost_method = 'fifo' + + self.env['mrp.bom'].create({ + 'product_tmpl_id': kit.product_tmpl_id.id, + 'product_qty': 1.0, + 'type': 'phantom', + 'bom_line_ids': [(0, 0, { + 'product_id': self.component_a.id, + 'product_qty': 1.0, + })] + }) + + self.component_a.standard_price = 10 + kit.button_bom_cost() + + stock_location = self.company_data['default_warehouse'].lot_stock_id + self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1) + + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.partner_a + with so_form.order_line.new() as line: + line.product_id = kit + so = so_form.save() + so.action_confirm() + + line = so.order_line + price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids) + self.assertEqual(price, 10) + + picking = so.picking_ids + action = picking.button_validate() + wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() + wizard.process() + + ctx = {'active_ids':picking.ids, 'active_id': picking.ids[0], 'active_model': 'stock.picking'} + return_picking_wizard_form = Form(self.env['stock.return.picking'].with_context(ctx)) + return_picking_wizard = return_picking_wizard_form.save() + return_picking_wizard.create_returns() + + price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids) + self.assertEqual(price, 10)