From 7b1acdf41a68fde7325513b387db29d0076e7cc5 Mon Sep 17 00:00:00 2001 From: "Anh Thao Pham (pta)" Date: Mon, 21 Jun 2021 14:21:35 +0000 Subject: [PATCH] [FIX] sale_mrp: fix COGS aml for products with kit BOM - Install stock, account_accountant, sale_management & mrp - Create a storable product (i.e. Kit A) - Configure its Product Category as followed: * Costing Method: Standard Price * Inventory Valuation: Automated - Create a consumable product (i.e. Kit B) - Create a consumable product with cost $3 (i.e. Component A) - Create a consumable product with cost $4 (i.e. Component B) - Create a consumable product with cost $5 (i.e. Component BB) - Create BOM for Kit B: * BOM Type: Kit * Quantity: 10 * Components: 2 x Component B 3 x Component BB - Create BOM for Kit A: * BOM Type: Kit * Quantity: 3 * Components: 2 x Kit B 1 x Component A - Create a SO with 1 Kit A and confirm it - Create an invoice and confirm it Values of the COGS aml created for Kit A ($26) do not match its BOM cost ($2.53). The method computing the price unit does not take into account BOM quantity: 10 for Kit B and 3 for Kit A. It only sums the cost of the components of BOM. Quantities of the components used by sub-kits should be related to the quantities defined in the BOM. Also, the average price unit computed for the BOM should be divided by the BOM quantity to get the true average price for one unit (BOM cost). opw-2557198 closes odoo/odoo#72812 X-original-commit: 36974be7cb46712f80d137b0a35646e77db910d8 Signed-off-by: William Henrotin Signed-off-by: Anh Thao PHAM --- addons/sale_mrp/models/account_move.py | 2 +- addons/sale_mrp/models/sale.py | 2 +- addons/sale_mrp/tests/__init__.py | 1 + .../sale_mrp/tests/test_sale_mrp_kit_bom.py | 166 ++++++++++++++++++ 4 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 addons/sale_mrp/tests/test_sale_mrp_kit_bom.py diff --git a/addons/sale_mrp/models/account_move.py b/addons/sale_mrp/models/account_move.py index 9f28147aa3d..6f338835bc1 100644 --- a/addons/sale_mrp/models/account_move.py +++ b/addons/sale_mrp/models/account_move.py @@ -25,7 +25,7 @@ class AccountMoveLine(models.Model): prod_qty_invoiced = factor * qty_invoiced prod_qty_to_invoice = factor * qty_to_invoice average_price_unit += factor * product._compute_average_price(prod_qty_invoiced, prod_qty_to_invoice, prod_moves) - price_unit = average_price_unit or price_unit + price_unit = average_price_unit / bom.product_qty or price_unit price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom_id) return price_unit diff --git a/addons/sale_mrp/models/sale.py b/addons/sale_mrp/models/sale.py index fbdc1b15cf5..b0d5733aa81 100644 --- a/addons/sale_mrp/models/sale.py +++ b/addons/sale_mrp/models/sale.py @@ -115,7 +115,7 @@ class SaleOrderLine(models.Model): for line, line_data in lines: product = line.product_id.id uom = line.product_uom_id - qty = line.product_qty + qty = line_data['qty'] if components.get(product, False): if uom.id != components[product]['uom']: from_uom = uom diff --git a/addons/sale_mrp/tests/__init__.py b/addons/sale_mrp/tests/__init__.py index f0acb254e5a..56a3cc3f15e 100644 --- a/addons/sale_mrp/tests/__init__.py +++ b/addons/sale_mrp/tests/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_sale_mrp_flow +from . import test_sale_mrp_kit_bom from . import test_sale_mrp_lead_time from . import test_sale_mrp_procurement from . import test_multistep_manufacturing \ No newline at end of file diff --git a/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py b/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py new file mode 100644 index 00000000000..39e8f167eb1 --- /dev/null +++ b/addons/sale_mrp/tests/test_sale_mrp_kit_bom.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase, Form + + +class TestSaleMrpKitBom(TransactionCase): + + def _create_product(self, name, product_type, price): + return self.env['product.product'].create({ + 'name': name, + 'type': product_type, + 'standard_price': price, + }) + + def test_sale_mrp_kit_bom_cogs(self): + """Check invoice COGS aml after selling and delivering a product + with Kit BoM having another product with Kit BoM as component""" + + # ---------------------------------------------- + # BoM of Kit A: + # - BoM Type: Kit + # - Quantity: 3 + # - Components: + # * 2 x Kit B + # * 1 x Component A (Cost: $3) + # + # BoM of Kit B: + # - BoM Type: Kit + # - Quantity: 10 + # - Components: + # * 2 x Component B (Cost: $4) + # * 3 x Component BB (Cost: $5) + # ---------------------------------------------- + + self.env.user.company_id.anglo_saxon_accounting = True + self.stock_input_account = self.env['account.account'].create({ + 'name': 'Stock Input', + 'code': 'StockIn', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.stock_output_account = self.env['account.account'].create({ + 'name': 'Stock Output', + 'code': 'StockOut', + 'reconcile': True, + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.stock_valuation_account = self.env['account.account'].create({ + 'name': 'Stock Valuation', + 'code': 'StockVal', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.expense_account = self.env['account.account'].create({ + 'name': 'Expense Account', + 'code': 'Exp', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + }) + self.income_account = self.env['account.account'].create({ + 'name': 'Income Account', + 'code': 'Inc', + 'user_type_id': self.env.ref('account.data_account_type_expenses').id, + }) + self.stock_journal = self.env['account.journal'].create({ + 'name': 'Stock Journal', + 'code': 'STJTEST', + 'type': 'general', + }) + self.recv_account = self.env['account.account'].create({ + 'name': 'account receivable', + 'code': 'RECV', + 'user_type_id': self.env.ref('account.data_account_type_receivable').id, + 'reconcile': True, + }) + self.pay_account = self.env['account.account'].create({ + 'name': 'account payable', + 'code': 'PAY', + 'user_type_id': self.env.ref('account.data_account_type_payable').id, + 'reconcile': True, + }) + self.customer = self.env['res.partner'].create({ + 'name': 'customer', + 'property_account_receivable_id': self.recv_account.id, + 'property_account_payable_id': self.pay_account.id, + }) + self.journal_sale = self.env['account.journal'].create({ + 'name': 'Sale Journal - Test', + 'code': 'AJ-SALE', + 'type': 'sale', + 'company_id': self.env.user.company_id.id, + }) + + self.component_a = self._create_product('Component A', 'consu', 3.00) + self.component_b = self._create_product('Component B', 'consu', 4.00) + self.component_bb = self._create_product('Component BB', 'consu', 5.00) + self.kit_a = self._create_product('Kit A', 'product', 0.00) + self.kit_b = self._create_product('Kit B', 'consu', 0.00) + + self.kit_a.write({ + 'categ_id': self.env.ref('product.product_category_all').id, + 'property_account_expense_id': self.expense_account.id, + 'property_account_income_id': self.income_account.id, + }) + self.kit_a.categ_id.write({ + 'property_stock_account_input_categ_id': self.stock_input_account.id, + 'property_stock_account_output_categ_id': self.stock_output_account.id, + 'property_stock_valuation_account_id': self.stock_valuation_account.id, + 'property_stock_journal': self.stock_journal.id, + 'property_valuation': 'real_time', + }) + + # Create BoM for Kit A + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.kit_a + bom_product_form.product_tmpl_id = self.kit_a.product_tmpl_id + bom_product_form.product_qty = 3.0 + bom_product_form.type = 'phantom' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.kit_b + bom_line.product_qty = 2.0 + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_a + bom_line.product_qty = 1.0 + self.bom_a = bom_product_form.save() + + # Create BoM for Kit B + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.kit_b + bom_product_form.product_tmpl_id = self.kit_b.product_tmpl_id + bom_product_form.product_qty = 10.0 + bom_product_form.type = 'phantom' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_b + bom_line.product_qty = 2.0 + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.component_bb + bom_line.product_qty = 3.0 + self.bom_b = bom_product_form.save() + + so = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'order_line': [ + (0, 0, { + 'name': self.kit_a.name, + 'product_id': self.kit_a.id, + 'product_uom_qty': 1.0, + 'product_uom': self.kit_a.uom_id.id, + 'price_unit': 1, + 'tax_id': False, + })], + }) + so.action_confirm() + so.picking_ids.move_lines.quantity_done = 1 + so.picking_ids.button_validate() + + invoice = so.with_context(default_journal_id=self.journal_sale.id)._create_invoices() + invoice.action_post() + + # Check the resulting accounting entries + amls = invoice.line_ids + self.assertEqual(len(amls), 4) + stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.stock_output_account) + self.assertEqual(stock_out_aml.debit, 0) + self.assertAlmostEqual(stock_out_aml.credit, 2.53) + cogs_aml = amls.filtered(lambda aml: aml.account_id == self.expense_account) + self.assertAlmostEqual(cogs_aml.debit, 2.53) + self.assertEqual(cogs_aml.credit, 0)