From 4cbeb0dade5465736fda8a889bae67b3396324ff Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Thu, 2 Feb 2023 19:04:32 +0100 Subject: [PATCH] [FIX] sale_mrp: BOM selection in anglosaxon price calculation We give preference to the bom in the stock moves for the sale order lines If there are changes in BOMs between the stock moves creation and the invoice validation a wrong price will be taken closes odoo/odoo#150506 X-original-commit: 855727eccb4ba9a2af72350b6ab9cc40ba7e50d9 Signed-off-by: William Henrotin (whe) --- addons/sale_mrp/models/account_move.py | 8 +- addons/sale_mrp/tests/test_sale_mrp_flow.py | 139 ++++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/addons/sale_mrp/models/account_move.py b/addons/sale_mrp/models/account_move.py index 220fa523634..7969143c066 100644 --- a/addons/sale_mrp/models/account_move.py +++ b/addons/sale_mrp/models/account_move.py @@ -11,8 +11,12 @@ class AccountMoveLine(models.Model): so_line = self.sale_line_ids and self.sale_line_ids[-1] or False if so_line: - bom = self.env['mrp.bom']._bom_find(products=so_line.product_id, company_id=so_line.company_id.id, bom_type='phantom')[so_line.product_id] - if bom: + # We give preference to the bom in the stock moves for the sale order lines + # If there are changes in BOMs between the stock moves creation and the + # invoice validation a wrong price will be taken + boms = so_line.move_ids.filtered(lambda m: m.state != 'cancel').mapped('bom_line_id.bom_id').filtered(lambda b: b.type == 'phantom') + if boms: + bom = boms[0] is_line_reversing = self.move_id.move_type == 'out_refund' qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing) diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index c6499f4b1a9..97877a7331e 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -1922,6 +1922,145 @@ class TestSaleMrpFlow(TestSaleMrpFlowCommon): self.assertEqual(aml_nokit_expense.debit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit") self.assertEqual(aml_nokit_output.credit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit") + def test_16_anglo_saxon_variant_price_unit_multi_company(self): + """ + Test the price unit of the BOM of the stock move is taken + Products: + Template A + variant KIT 1 + variant KIT 2 + Business Flow: + create SO + validate the delivery + archive the BOM and create a new one + create the invoice + post the invoice + """ + + # Create environment + self.partner = self.env['res.partner'].create({'name': 'Test Partner'}) + self.category = self.env.ref('product.product_category_1').copy({'name': 'Test category', 'property_valuation': 'real_time', 'property_cost_method': 'fifo'}) + account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00', 'account_type': 'asset_receivable', 'reconcile': True}) + account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00', 'account_type': 'asset_current', 'reconcile': True}) + account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00', 'account_type': 'liability_current', 'reconcile': True}) + account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00', 'account_type': 'liability_current', 'reconcile': True}) + account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'account_type': 'asset_receivable', 'reconcile': True}) + self.stock_location = self.company_data['default_warehouse'].lot_stock_id + self.partner.property_account_receivable_id = account_receiv + self.category.property_account_income_categ_id = account_income + self.category.property_account_expense_categ_id = account_expense + self.category.property_stock_account_input_categ_id = account_income + self.category.property_stock_account_output_categ_id = account_output + self.category.property_stock_valuation_account_id = account_valuation + + # Create variant attributes + self.prod_att_test = self.env['product.attribute'].create({'name': 'test'}) + self.prod_attr_KIT_A = self.env['product.attribute.value'].create({'name': 'KIT A', 'attribute_id': self.prod_att_test.id, 'sequence': 1}) + + # Create the template + self.product_template = self.env['product.template'].create({ + 'name': 'Template A', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'invoice_policy': 'delivery', + 'categ_id': self.category.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.prod_att_test.id, + 'value_ids': [(6, 0, [self.prod_attr_KIT_A.id])] + })] + }) + + # Create another variant + self.pt_attr_KIT_A = self.product_template.attribute_line_ids[0].product_template_value_ids[0] + self.variant_KIT_A = self.product_template._get_variant_for_combination(self.pt_attr_KIT_A) + # Assign a cost to the NOKIT variant + self.variant_KIT_A.write({'standard_price': 25}) + + # Create the components + self.comp_kit_a = self.env['product.product'].create({ + 'name': 'Component Kit A', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'categ_id': self.category.id, + 'standard_price': 20 + }) + self.comp_kit_b = self.env['product.product'].create({ + 'name': 'Component Kit B', + 'type': 'product', + 'uom_id': self.uom_unit.id, + 'categ_id': self.category.id, + 'standard_price': 10 + }) + + # Create the bom + bom = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.product_template.id, + 'product_id': self.variant_KIT_A.id, + 'product_qty': 1.0, + 'type': 'phantom', + 'company_id': self.env.company.id, + }) + self.env['mrp.bom.line'].create({ + 'product_id': self.comp_kit_a.id, + 'product_qty': 1.0, + 'company_id': self.env.company.id, + 'bom_id': bom.id + }) + + # Create the quants + self.env['stock.quant']._update_available_quantity(self.comp_kit_a, self.stock_location, 2) + self.env['stock.quant']._update_available_quantity(self.comp_kit_b, self.stock_location, 1) + + # Create the sale order + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, { + 'name': self.variant_KIT_A.name, + 'product_id': self.variant_KIT_A.id, + 'product_uom_qty': 1, + 'product_uom': self.uom_unit.id, + 'price_unit': 50 + })], + 'company_id': self.env.company.id, + } + so = self.env['sale.order'].create(so_vals) + # Validate the sale order + so.action_confirm() + # Deliver the products + pick = so.picking_ids + pick.button_validate() + # archive bOM and update it + bom.active = False + bom_updated = self.env['mrp.bom'].create({ + 'product_tmpl_id': self.product_template.id, + 'product_id': self.variant_KIT_A.id, + 'product_qty': 1.0, + 'type': 'phantom', + 'company_id': self.env.company.id, + }) + self.env['mrp.bom.line'].create({ + 'product_id': self.comp_kit_b.id, + 'product_qty': 1.0, + 'company_id': self.env.company.id, + 'bom_id': bom_updated.id + }) + + # Create the invoice + so._create_invoices() + # Validate the invoice + invoice = so.invoice_ids + invoice.action_post() + + amls = invoice.line_ids + aml_nokit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_KIT_A) + aml_nokit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_KIT_A) + + # Check that the Cost of Goods Sold for variant NOKIT is equal to the cost of the first BOM + self.assertEqual(aml_nokit_expense.debit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit") + self.assertEqual(aml_nokit_output.credit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit") + def test_reconfirm_cancelled_kit(self): so = self.env['sale.order'].create({ 'partner_id': self.env['res.partner'].create({'name': 'Test Partner'}).id,