[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 <Whenrow@users.noreply.github.com>
Signed-off-by: Anh Thao PHAM <kitan191@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user