mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-23 07:42:06 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -2,5 +2,5 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import test_analytic_account
|
||||
from . import test_mrp_account
|
||||
from . import test_bom_price
|
||||
from . import test_valuation_layers
|
||||
from . import test_valuation_operation
|
||||
|
|
|
|||
|
|
@ -1,40 +1,73 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common, Form
|
||||
from odoo.tools.float_utils import float_round, float_compare
|
||||
from odoo.tests import tagged, Form
|
||||
from odoo.addons.stock_account.tests.common import TestStockValuationCommon
|
||||
|
||||
|
||||
class TestBomPriceCommon(common.TransactionCase):
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestBomPriceCommon(TestStockValuationCommon):
|
||||
|
||||
@classmethod
|
||||
def _create_product(cls, name, price):
|
||||
return cls.Product.create({
|
||||
def _create_product(cls, name, price, quantity=100, category=None):
|
||||
vals = {
|
||||
'name': name,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'standard_price': price,
|
||||
'qty_available': quantity,
|
||||
}
|
||||
if category:
|
||||
vals['categ_id'] = category.id
|
||||
return cls.Product.create(vals)
|
||||
|
||||
@classmethod
|
||||
def _create_mo(cls, bom, quantity, confirm=True):
|
||||
mo = cls.env['mrp.production'].create({
|
||||
'product_id': bom.product_id.id,
|
||||
'bom_id': bom.id,
|
||||
'product_qty': quantity,
|
||||
})
|
||||
if confirm:
|
||||
mo.action_confirm()
|
||||
return mo
|
||||
|
||||
@classmethod
|
||||
def _produce(cls, mo, quantity=0):
|
||||
mo_form = Form(mo)
|
||||
if not quantity:
|
||||
quantity = mo.product_qty - mo.qty_produced
|
||||
mo_form.qty_producing += quantity
|
||||
return mo_form.save()
|
||||
|
||||
@classmethod
|
||||
def _use_production_accounting(cls):
|
||||
cls.account_production = cls.env['account.account'].create({
|
||||
'name': 'Production Account',
|
||||
'code': '100102',
|
||||
'account_type': 'asset_current',
|
||||
})
|
||||
production_locations = cls.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', cls.company.id)])
|
||||
production_locations.valuation_account_id = cls.account_production.id
|
||||
return cls.account_production
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Required for `product_uom_id ` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
cls.env.user.group_ids += cls.env.ref('uom.group_uom')
|
||||
# Required for `product_id ` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('product.group_product_variant')
|
||||
cls.env.user.group_ids += cls.env.ref('product.group_product_variant')
|
||||
cls.Product = cls.env['product.product']
|
||||
cls.Bom = cls.env['mrp.bom']
|
||||
cls.prod_location = cls.warehouse._get_production_location()
|
||||
|
||||
# Products.
|
||||
cls.dining_table = cls._create_product('Dining Table', 1000)
|
||||
cls.dining_table = cls._create_product('Dining Table', 1000, quantity=0, category=cls.category_fifo_auto)
|
||||
cls.table_head = cls._create_product('Table Head', 300)
|
||||
cls.screw = cls._create_product('Screw', 10)
|
||||
cls.leg = cls._create_product('Leg', 25)
|
||||
cls.glass = cls._create_product('Glass', 100)
|
||||
cls.glass = cls._create_product('Glass', 100, quantity=0, category=cls.category_avco_auto)
|
||||
|
||||
# Unit of Measure.
|
||||
cls.unit = cls.env.ref("uom.product_uom_unit")
|
||||
cls.dozen = cls.env.ref("uom.product_uom_dozen")
|
||||
|
||||
# Bills Of Materials.
|
||||
|
|
@ -51,7 +84,7 @@ class TestBomPriceCommon(common.TransactionCase):
|
|||
bom_form.product_id = cls.dining_table
|
||||
bom_form.product_tmpl_id = cls.dining_table.product_tmpl_id
|
||||
bom_form.product_qty = 1.0
|
||||
bom_form.product_uom_id = cls.unit
|
||||
bom_form.product_uom_id = cls.uom
|
||||
bom_form.type = 'normal'
|
||||
with bom_form.bom_line_ids.new() as line:
|
||||
line.product_id = cls.table_head
|
||||
|
|
@ -102,85 +135,36 @@ class TestBomPriceCommon(common.TransactionCase):
|
|||
line.product_id = cls.corner_slide
|
||||
line.product_qty = 57
|
||||
cls.bom_2 = bom_form2.save()
|
||||
cls._use_production_accounting()
|
||||
|
||||
|
||||
class TestBomPrice(TestBomPriceCommon):
|
||||
def test_00_compute_price(self):
|
||||
"""Test multi-level BoM cost"""
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.dining_table.button_bom_cost()
|
||||
self.assertEqual(self.dining_table.standard_price, 550, "After computing price from BoM price should be 550")
|
||||
class TestBomPriceOperationCommon(TestBomPriceCommon):
|
||||
""" Common bom setup with workorder operations"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
def test_01_compute_price_operation_cost(self):
|
||||
"""Test calcuation of bom cost with operations."""
|
||||
workcenter_form1 = Form(self.env['mrp.workcenter'])
|
||||
workcenter_form1.name = 'Workcenter'
|
||||
workcenter_form1.time_efficiency = 80
|
||||
workcenter_form1.default_capacity = 2
|
||||
workcenter_form1.oee_target = 100
|
||||
workcenter_form1.time_start = 15
|
||||
workcenter_form1.time_stop = 15
|
||||
workcenter_form1.costs_hour = 100
|
||||
workcenter_1 = workcenter_form1.save()
|
||||
|
||||
self.env['mrp.workcenter.capacity'].create({
|
||||
'product_id': self.dining_table.id,
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_start': 2,
|
||||
'time_stop': 1,
|
||||
cls.env.user.write({'group_ids': [(4, cls.env.ref('mrp.group_mrp_routings').id)]})
|
||||
cls.account_expense_wo = cls.env['account.account'].create({
|
||||
'code': 'X2120',
|
||||
'name': 'WO - Expenses',
|
||||
'account_type': 'expense',
|
||||
})
|
||||
cls.workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Workcenter',
|
||||
'time_efficiency': 80,
|
||||
'oee_target': 100,
|
||||
'time_start': 15,
|
||||
'time_stop': 15,
|
||||
'costs_hour': 100,
|
||||
'expense_account_id': cls.account_expense.id,
|
||||
})
|
||||
cls.env['mrp.workcenter.capacity'].create({
|
||||
'product_id': cls.dining_table.id,
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_start': 17,
|
||||
'time_stop': 16,
|
||||
})
|
||||
|
||||
self.bom_1.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
self.bom_2.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Dinning Table Operation Cost(1 Unit)
|
||||
|
|
@ -194,7 +178,6 @@ class TestBomPrice(TestBomPriceCommon):
|
|||
# Operation Cost 1 unit = 321.25
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Table Head Operation Cost (1 Dozen)
|
||||
# --------------------------------------------------------------------------
|
||||
|
|
@ -206,40 +189,58 @@ class TestBomPrice(TestBomPriceCommon):
|
|||
# ----------------------------------------
|
||||
# Operation Cost 1 dozen (306.25 + 15 = 321.25 per dozen) and 25.52 for 1 Unit
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.dining_table.button_bom_cost()
|
||||
# Total cost of Dining Table = (550) + Total cost of operations (321.25) = 871.25
|
||||
self.assertEqual(float_round(self.dining_table.standard_price, precision_digits=2), 871.25, "After computing price from BoM price should be 871.25")
|
||||
self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost()
|
||||
# Total cost of Dining Table = (718.75) + Total cost of all operations (321.25 + 25.52) = 1065.52
|
||||
self.assertEqual(float_compare(self.dining_table.standard_price, 1065.52, precision_digits=2), 0, "After computing price from BoM price should be 1065.52")
|
||||
|
||||
def test_02_compute_byproduct_price(self):
|
||||
"""Test BoM cost when byproducts with cost share"""
|
||||
# byproduct
|
||||
scrap_wood = self._create_product('Scrap Wood', 30)
|
||||
|
||||
# different byproduct line uoms => 20 total units with a total of 75% of cost share
|
||||
self.bom_1.write({
|
||||
'byproduct_ids': [
|
||||
cls.bom_1.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'product_id': scrap_wood.id,
|
||||
'product_uom_id': self.unit.id,
|
||||
'product_qty': 8,
|
||||
'bom_id': self.bom_1.id,
|
||||
'cost_share': 25,
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': scrap_wood.id,
|
||||
'product_uom_id': self.dozen.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': self.bom_1.id,
|
||||
'cost_share': 50,
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
cls.bom_2.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
# byproduct
|
||||
|
||||
# Cost Breakdown.
|
||||
# -------------------------------------------------------------------------------
|
||||
|
|
@ -248,9 +249,24 @@ class TestBomPrice(TestBomPriceCommon):
|
|||
# Scrap Wood 1 Unit = (25 + 50) / 100 * 550 / (8 units + 12 units) = 20.625
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.assertEqual(scrap_wood.standard_price, 30, "Initial price of the By-Product should be 30")
|
||||
self.dining_table.button_bom_cost()
|
||||
self.assertEqual(self.dining_table.standard_price, 137.5, "After computing price from BoM price should be 137.5")
|
||||
scrap_wood.button_bom_cost()
|
||||
self.assertEqual(scrap_wood.standard_price, 20.63, "After computing price from BoM price should be 20.63")
|
||||
cls.scrap_wood = cls._create_product('Scrap Wood', 30, quantity=0)
|
||||
|
||||
# different byproduct line uoms => 20 total units with a total of 75% of cost share
|
||||
cls.bom_1.write({
|
||||
'byproduct_ids': [
|
||||
(0, 0, {
|
||||
'product_id': cls.scrap_wood.id,
|
||||
'product_uom_id': cls.uom.id,
|
||||
'product_qty': 8,
|
||||
'bom_id': cls.bom_1.id,
|
||||
'cost_share': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': cls.scrap_wood.id,
|
||||
'product_uom_id': cls.dozen.id,
|
||||
'product_qty': 1,
|
||||
'bom_id': cls.bom_1.id,
|
||||
'cost_share': 12,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -1,339 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestMrpAnalyticAccount(TransactionCase):
|
||||
class TestAnalyticAccount(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.write({'groups_id': [(4, cls.env.ref('analytic.group_analytic_accounting').id),]})
|
||||
# The group 'mrp.group_mrp_routings' is required to make the field
|
||||
# 'workorder_ids' visible in the view of 'mrp.production'. The subviews
|
||||
# of `workorder_ids` must be present in many tests to create records.
|
||||
cls.env.user.group_ids += (
|
||||
cls.env.ref('analytic.group_analytic_accounting')
|
||||
+ cls.env.ref('mrp.group_mrp_routings')
|
||||
)
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan',
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
'name': 'test_analytic_account',
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
})
|
||||
cls.workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Workcenter',
|
||||
'default_capacity': 1,
|
||||
'time_efficiency': 100,
|
||||
'costs_hour': 10,
|
||||
cls.applicability = cls.env['account.analytic.applicability'].create({
|
||||
'business_domain': 'general',
|
||||
'analytic_plan_id': cls.analytic_plan.id,
|
||||
'applicability': 'mandatory',
|
||||
})
|
||||
cls.product = cls.env['product.product'].create({
|
||||
'name': 'Product',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'standard_price': 233.0,
|
||||
})
|
||||
cls.component = cls.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
'standard_price': 10.0,
|
||||
})
|
||||
cls.bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product.id,
|
||||
'product_tmpl_id': cls.product.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.component.id, 'product_qty': 1.0}),
|
||||
],
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'work work', 'workcenter_id': cls.workcenter.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
]})
|
||||
|
||||
|
||||
class TestAnalyticAccount(TestMrpAnalyticAccount):
|
||||
def test_mo_analytic(self):
|
||||
"""Test the amount on analytic line will change when consumed qty of the
|
||||
component changed.
|
||||
def test_mandatory_analytic_plan_bom(self):
|
||||
"""
|
||||
# create a mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product
|
||||
mo_form.bom_id = self.bom
|
||||
mo_form.product_qty = 10.0
|
||||
mo_form.analytic_account_id = self.analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 0)
|
||||
# increase qty_producing to 5.0
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.state, 'progress')
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -50.0)
|
||||
|
||||
# increase qty_producing to 10.0
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 10.0
|
||||
mo_form.save()
|
||||
# Hack to bypass test doing strange things
|
||||
mo._set_qty_producing()
|
||||
mo.workorder_ids.button_finish()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -100.0)
|
||||
|
||||
# mark as done
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -100.0)
|
||||
|
||||
def test_mo_analytic_backorder(self):
|
||||
"""Test the analytic lines are correctly posted when backorder.
|
||||
Tests that the distribution validation is correctly evaluated
|
||||
The BOM creation should not be constrained by any analytic applicability rule.
|
||||
"""
|
||||
# create a mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product
|
||||
mo_form.bom_id = self.bom
|
||||
mo_form.product_qty = 10.0
|
||||
mo_form.analytic_account_id = self.analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 0)
|
||||
|
||||
# increase qty_producing to 5.0
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 5.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.state, 'progress')
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -50.0)
|
||||
|
||||
backorder_wizard_dict = mo.button_mark_done()
|
||||
Form(self.env[(backorder_wizard_dict.get('res_model'))].with_context(backorder_wizard_dict['context'])).save().action_backorder()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -50.0)
|
||||
|
||||
def test_workcenter_same_analytic_account(self):
|
||||
"""Test when workcenter and MO are using the same analytic account, no
|
||||
duplicated lines will be post.
|
||||
"""
|
||||
# Required for `workorder_ids` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
|
||||
# set wc analytic account to be the same of the one on the bom
|
||||
self.workcenter.costs_hour_account_id = self.analytic_account
|
||||
|
||||
# create a mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product
|
||||
mo_form.bom_id = self.bom
|
||||
mo_form.product_qty = 10.0
|
||||
mo_form.analytic_account_id = self.analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
|
||||
|
||||
# change duration to 60
|
||||
mo_form = Form(mo)
|
||||
with mo_form.workorder_ids.edit(0) as line_edit:
|
||||
line_edit.duration = 60.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -10.0)
|
||||
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
|
||||
|
||||
# change duration to 120
|
||||
with mo_form.workorder_ids.edit(0) as line_edit:
|
||||
line_edit.duration = 120.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
|
||||
|
||||
# mark as done
|
||||
mo_form.qty_producing = 10.0
|
||||
mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
|
||||
|
||||
def test_workcenter_different_analytic_account(self):
|
||||
"""Test when workcenter and MO are using the same analytic account, no
|
||||
duplicated lines will be post.
|
||||
"""
|
||||
# Required for `workorder_ids` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
|
||||
# set wc analytic account to be different from the one on the bom
|
||||
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
|
||||
wc_analytic_account = self.env['account.analytic.account'].create({'name': 'wc_analytic_account', 'plan_id': analytic_plan.id})
|
||||
self.workcenter.costs_hour_account_id = wc_analytic_account
|
||||
|
||||
# create a mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product
|
||||
mo_form.bom_id = self.bom
|
||||
mo_form.product_qty = 10.0
|
||||
mo_form.analytic_account_id = self.analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
|
||||
|
||||
# change duration to 60
|
||||
mo_form = Form(mo)
|
||||
with mo_form.workorder_ids.edit(0) as line_edit:
|
||||
line_edit.duration = 60.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -10.0)
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -10.0)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
|
||||
|
||||
# change duration to 120
|
||||
with mo_form.workorder_ids.edit(0) as line_edit:
|
||||
line_edit.duration = 120.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
|
||||
|
||||
# mark as done
|
||||
mo_form.qty_producing = 10.0
|
||||
mo_form.save()
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -20.0)
|
||||
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
|
||||
|
||||
def test_changing_mo_analytic_account(self):
|
||||
""" Check if the MO account analytic lines are correctly updated
|
||||
after the change of the MO account analytic.
|
||||
"""
|
||||
# Required for `workorder_ids` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
|
||||
# create a mo
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product
|
||||
mo_form.bom_id = self.bom
|
||||
mo_form.product_qty = 1
|
||||
mo_form.analytic_account_id = self.analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 0)
|
||||
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 0)
|
||||
|
||||
# Change duration to 60
|
||||
mo_form = Form(mo)
|
||||
with mo_form.workorder_ids.edit(0) as line_edit:
|
||||
line_edit.duration = 60.0
|
||||
mo_form.save()
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
|
||||
|
||||
# Mark as done
|
||||
wizard_dict = mo.button_mark_done()
|
||||
Form(self.env[(wizard_dict.get('res_model'))].with_context(wizard_dict['context'])).save().process()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 1)
|
||||
|
||||
# Create a new analytic account
|
||||
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
|
||||
new_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account_2', 'plan_id': analytic_plan.id})
|
||||
# Change the MO analytic account
|
||||
mo.analytic_account_id = new_analytic_account
|
||||
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.account_id.id, new_analytic_account.id)
|
||||
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id.id, new_analytic_account.id)
|
||||
|
||||
#Get the MO analytic account lines
|
||||
mo_analytic_account_raw_lines = mo.move_raw_ids.analytic_account_line_id
|
||||
mo_analytic_account_wc_lines = mo.workorder_ids.mo_analytic_account_line_id
|
||||
mo.analytic_account_id = False
|
||||
# Check that the MO analytic account lines are deleted
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 0)
|
||||
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 0)
|
||||
self.assertFalse(mo_analytic_account_raw_lines.exists())
|
||||
self.assertFalse(mo_analytic_account_wc_lines.exists())
|
||||
# Check that the AA lines are recreated correctly if we delete the AA, save the MO, and assign a new one
|
||||
mo.analytic_account_id = self.analytic_account
|
||||
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 1)
|
||||
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 1)
|
||||
|
||||
def test_add_remove_wo_analytic_no_company(self):
|
||||
"""Test the addition and removal of work orders to a MO linked to
|
||||
an analytic account that has no company associated
|
||||
"""
|
||||
# Create an analytic account and remove the company
|
||||
analytic_account_no_company = self.env['account.analytic.account'].create({
|
||||
'name': 'test_analytic_account_no_company',
|
||||
'plan_id': self.analytic_plan.id,
|
||||
})
|
||||
analytic_account_no_company.company_id = False
|
||||
|
||||
# Create a mo linked to an analytic account with no associated company
|
||||
mo_no_company = self.env['mrp.production'].create({
|
||||
'product_id': self.product.id,
|
||||
'analytic_account_id': analytic_account_no_company.id,
|
||||
'product_uom_id': self.bom.product_uom_id.id,
|
||||
})
|
||||
|
||||
mo_no_c_form = Form(mo_no_company)
|
||||
wo = self.env['mrp.workorder'].create({
|
||||
'name': 'Work_order',
|
||||
'workcenter_id': self.workcenter.id,
|
||||
'product_uom_id': self.bom.product_uom_id.id,
|
||||
'production_id': mo_no_c_form.id,
|
||||
'duration': 60,
|
||||
})
|
||||
mo_no_c_form.save()
|
||||
self.assertTrue(mo_no_company.workorder_ids)
|
||||
self.assertEqual(wo.production_id.analytic_account_id, analytic_account_no_company)
|
||||
self.assertEqual(len(analytic_account_no_company.line_ids), 1)
|
||||
mo_no_company.workorder_ids.unlink()
|
||||
self.assertEqual(len(analytic_account_no_company.line_ids), 0)
|
||||
|
||||
def test_update_components_qty_to_0(self):
|
||||
""" Test that the analytic lines are deleted when the quantity of the component is set to 0.
|
||||
Create a Mo with analytic account and a component, confirm and validate it,
|
||||
set the quantity of the component to 0, the analytic lines should be deleted.
|
||||
"""
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
'standard_price': 100,
|
||||
})
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Product',
|
||||
'type': 'product',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
})],
|
||||
'product_tmpl_id': self.product.product_tmpl_id.id,
|
||||
})
|
||||
analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': "Test Account",
|
||||
'plan_id': self.analytic_plan.id,
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 1.0
|
||||
mo_form.analytic_account_id = analytic_account
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertTrue(bom)
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(analytic_account.debit, 100)
|
||||
mo.move_raw_ids[0].quantity_done = 0
|
||||
self.assertEqual(analytic_account.debit, 0)
|
||||
self.assertFalse(analytic_account.line_ids)
|
||||
self.applicability.business_domain = 'manufacturing_order'
|
||||
|
||||
bom_2 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.product.product_tmpl_id.id,
|
||||
})
|
||||
self.assertTrue(bom_2)
|
||||
|
||||
def test_mandatory_analytic_plan_workcenter(self):
|
||||
"""
|
||||
Tests that the distribution validation is correctly evaluated
|
||||
The Workcenter creation should not be constrained by any analytic applicability rule.
|
||||
"""
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'name': "Great Workcenter",
|
||||
'analytic_distribution': False,
|
||||
})
|
||||
self.assertTrue(workcenter)
|
||||
|
||||
self.applicability.business_domain = 'manufacturing_order'
|
||||
|
||||
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||
'name': "Great Workcenter",
|
||||
'analytic_distribution': False,
|
||||
})
|
||||
self.assertTrue(workcenter_2)
|
||||
|
|
|
|||
|
|
@ -1,179 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.addons.stock_account.tests.test_account_move import TestAccountMoveStockCommon
|
||||
from odoo.tests import Form, tagged
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo.addons.mrp_account.tests.common import TestBomPriceCommon, TestBomPriceOperationCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import new_test_user
|
||||
from odoo.tools import float_compare, float_round
|
||||
from odoo import fields
|
||||
|
||||
|
||||
class TestMrpAccount(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpAccount, cls).setUpClass()
|
||||
cls.source_location_id = cls.stock_location_14.id
|
||||
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||||
# setting up alternative workcenters
|
||||
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter bis',
|
||||
'default_capacity': 3,
|
||||
'time_start': 9,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter ter',
|
||||
'default_capacity': 1,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 85,
|
||||
})
|
||||
cls.product_4.uom_id = cls.uom_unit
|
||||
cls.planning_bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
]})
|
||||
cls.dining_table = cls.env['product.product'].create({
|
||||
'name': 'Table (MTO)',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_sheet = cls.env['product.product'].create({
|
||||
'name': 'Table Top',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_leg = cls.env['product.product'].create({
|
||||
'name': 'Table Leg',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.product_bolt = cls.env['product.product'].create({
|
||||
'name': 'Bolt',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.product_screw = cls.env['product.product'].create({
|
||||
'name': 'Screw',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_bom_desk = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
|
||||
],
|
||||
})
|
||||
cls.mrp_bom_desk.write({
|
||||
'bom_line_ids': [
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_sheet.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 1,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_leg.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 2,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_bolt.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_screw.id,
|
||||
'product_qty': 10,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 4,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
]
|
||||
})
|
||||
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Drill Station 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.categ_standard = cls.env['product.category'].create({
|
||||
'name': 'STANDARD',
|
||||
'property_cost_method': 'standard'
|
||||
})
|
||||
cls.categ_real = cls.env['product.category'].create({
|
||||
'name': 'REAL',
|
||||
'property_cost_method': 'fifo'
|
||||
})
|
||||
cls.categ_average = cls.env['product.category'].create({
|
||||
'name': 'AVERAGE',
|
||||
'property_cost_method': 'average'
|
||||
})
|
||||
cls.dining_table.categ_id = cls.categ_real.id
|
||||
cls.product_table_sheet.categ_id = cls.categ_real.id
|
||||
cls.product_table_leg.categ_id = cls.categ_average.id
|
||||
cls.product_bolt.categ_id = cls.categ_standard.id
|
||||
cls.product_screw.categ_id = cls.categ_standard.id
|
||||
cls.env['stock.move'].search([('product_id', 'in', [cls.product_bolt.id, cls.product_screw.id])])._do_unreserve()
|
||||
(cls.product_bolt + cls.product_screw).write({'type': 'product'})
|
||||
cls.dining_table.tracking = 'none'
|
||||
class TestMrpAccount(TestBomPriceCommon):
|
||||
|
||||
def test_00_production_order_with_accounting(self):
|
||||
self.product_table_sheet.standard_price = 20.0
|
||||
self.product_table_leg.standard_price = 15.0
|
||||
self.product_bolt.standard_price = 10.0
|
||||
self.product_screw.standard_price = 0.1
|
||||
self.product_table_leg.tracking = 'none'
|
||||
self.product_table_sheet.tracking = 'none'
|
||||
# Inventory Product Table
|
||||
quants = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_table_sheet.id, # tracking serial
|
||||
quants = self.env['stock.quant'].with_context(inventory_mode=True).create([{
|
||||
'product_id': self.leg.id,
|
||||
'inventory_quantity': 20,
|
||||
'location_id': self.source_location_id,
|
||||
})
|
||||
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_table_leg.id, # tracking lot
|
||||
'location_id': self.stock_location.id,
|
||||
}, {
|
||||
'product_id': self.glass.id,
|
||||
'inventory_quantity': 20,
|
||||
'location_id': self.source_location_id,
|
||||
})
|
||||
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_bolt.id,
|
||||
'inventory_quantity': 20,
|
||||
'location_id': self.source_location_id,
|
||||
})
|
||||
quants |= self.env['stock.quant'].create({
|
||||
'product_id': self.product_screw.id,
|
||||
'location_id': self.stock_location.id,
|
||||
}, {
|
||||
'product_id': self.screw.id,
|
||||
'inventory_quantity': 200000,
|
||||
'location_id': self.source_location_id,
|
||||
})
|
||||
'location_id': self.stock_location.id,
|
||||
}])
|
||||
quants.action_apply_inventory()
|
||||
|
||||
bom = self.mrp_bom_desk.copy()
|
||||
bom.bom_line_ids.manual_consumption = False
|
||||
bom.operation_ids = False
|
||||
production_table_form = Form(self.env['mrp.production'])
|
||||
production_table_form.product_id = self.dining_table
|
||||
production_table_form.bom_id = bom
|
||||
production_table_form.bom_id = self.bom_1
|
||||
production_table_form.product_qty = 1
|
||||
production_table = production_table_form.save()
|
||||
|
||||
|
|
@ -183,172 +41,421 @@ class TestMrpAccount(TestMrpCommon):
|
|||
mo_form = Form(production_table)
|
||||
mo_form.qty_producing = 1
|
||||
production_table = mo_form.save()
|
||||
production_table._post_inventory()
|
||||
move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").stock_valuation_layer_ids.value
|
||||
production_table.button_mark_done()
|
||||
move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").value
|
||||
|
||||
# 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + 1*20 (extra cost)
|
||||
self.assertEqual(move_value, 141, 'Thing should have the correct price')
|
||||
# 1 table head at 468.75 + 4 table leg at 25 + 1 glass at 100 + 5 screw at 10 + 1*20 (extra cost)
|
||||
self.assertEqual(move_value, 738.75, 'Thing should have the correct price')
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestMrpAccountMove(TestAccountMoveStockCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.product_B = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Product B",
|
||||
"type": "product",
|
||||
"default_code": "prda",
|
||||
"categ_id": cls.auto_categ.id,
|
||||
"taxes_id": [(5, 0, 0)],
|
||||
"supplier_taxes_id": [(5, 0, 0)],
|
||||
"lst_price": 100.0,
|
||||
"standard_price": 10.0,
|
||||
"property_account_income_id": cls.company_data["default_account_revenue"].id,
|
||||
"property_account_expense_id": cls.company_data["default_account_expense"].id,
|
||||
}
|
||||
def test_stock_user_without_account_permissions_can_create_bom(self):
|
||||
mrp_manager = new_test_user(
|
||||
self.env, 'temp_mrp_manager', 'mrp.group_mrp_manager,product.group_product_variant',
|
||||
)
|
||||
|
||||
bom_form = Form(self.env['mrp.bom'].with_user(mrp_manager))
|
||||
bom_form.product_id = self.dining_table
|
||||
|
||||
def test_two_productions_unbuild_one_sell_other_fifo(self):
|
||||
""" Unbuild orders, when supplied with a specific MO record, should restrict their value
|
||||
consumption to moves originating from that MO record.
|
||||
"""
|
||||
mo_1 = self._create_mo(self.bom_1, 1)
|
||||
mo_1.action_confirm()
|
||||
mo_1.action_assign()
|
||||
mo_1.button_mark_done()
|
||||
self.assertRecordValues(
|
||||
self.env['stock.move'].search([('product_id', '=', self.dining_table.id)]),
|
||||
# MO_1
|
||||
[{'remaining_qty': 1.0, 'value': 718.75}],
|
||||
)
|
||||
|
||||
self.plywood_sheet.standard_price = 400
|
||||
mo_2 = self._create_mo(self.bom_1, 1)
|
||||
mo_2.action_confirm()
|
||||
mo_2.action_assign()
|
||||
mo_2.button_mark_done()
|
||||
self.assertRecordValues(
|
||||
self.env['stock.move'].search([('product_id', '=', self.dining_table.id)]),
|
||||
[
|
||||
{'remaining_qty': 1.0, 'value': 718.75},
|
||||
# MO_2 new value to reflect change of component's `standard_price`
|
||||
{'remaining_qty': 1.0, 'value': 918.75},
|
||||
],
|
||||
)
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.product_id = self.dining_table
|
||||
unbuild_form.bom_id = self.bom_1
|
||||
unbuild_form.product_qty = 1
|
||||
unbuild_form.mo_id = mo_2
|
||||
unbuild_order = unbuild_form.save()
|
||||
unbuild_order.action_unbuild()
|
||||
self.assertRecordValues(
|
||||
self.env['stock.move'].search([('product_id', '=', self.dining_table.id)]),
|
||||
[
|
||||
{'remaining_qty': 0.0, 'value': 718.75, 'quantity': 1.0},
|
||||
{'remaining_qty': 1.0, 'value': 918.75, 'quantity': 1.0},
|
||||
# Unbuild move value is derived from MO_2, as precised on the unbuild form
|
||||
{'remaining_qty': 0.0, 'value': 718.75, 'quantity': 1.0},
|
||||
],
|
||||
)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertRecordValues(
|
||||
self.env['stock.move'].search([('product_id', '=', self.dining_table.id)]),
|
||||
[
|
||||
{'remaining_qty': 0.0, 'value': 718.75, 'quantity': 1.0},
|
||||
{'remaining_qty': 0.0, 'value': 918.75, 'quantity': 1.0},
|
||||
{'remaining_qty': 0.0, 'value': 718.75, 'quantity': 1.0},
|
||||
# Out move value is derived from MO_1, the only candidate origin with some `remaining_qty`
|
||||
{'remaining_qty': 0.0, 'value': 918.75, 'quantity': 1.0},
|
||||
],
|
||||
)
|
||||
cls.bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_A.id,
|
||||
'product_tmpl_id': cls.product_A.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_B.id, 'product_qty': 1}),
|
||||
]})
|
||||
|
||||
def test_unbuild_account_00(self):
|
||||
"""Test when after unbuild, the journal entries are the reversal of the
|
||||
journal entries created when produce the product.
|
||||
"""
|
||||
# build
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.product_A
|
||||
production_form.bom_id = self.bom
|
||||
production_form.product_qty = 1
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
production = mo_form.save()
|
||||
production._post_inventory()
|
||||
production = self._create_mo(self.bom_1, 1)
|
||||
production.button_mark_done()
|
||||
|
||||
# finished product move
|
||||
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('credit', '=', 0)])
|
||||
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('debit', '=', 0)])
|
||||
self.assertEqual(productA_debit_line.account_id, self.stock_valuation_account)
|
||||
self.assertEqual(productA_credit_line.account_id, self.stock_input_account)
|
||||
# component move
|
||||
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('credit', '=', 0)])
|
||||
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('debit', '=', 0)])
|
||||
self.assertEqual(productB_debit_line.account_id, self.stock_output_account)
|
||||
self.assertEqual(productB_credit_line.account_id, self.stock_valuation_account)
|
||||
productA_debit_line = self.env['account.move.line'].search([('product_id', '=', self.dining_table.id), ('credit', '=', 0)])
|
||||
productA_credit_line = self.env['account.move.line'].search([('product_id', '=', self.dining_table.id), ('debit', '=', 0)])
|
||||
self.assertEqual(productA_debit_line.account_id, self.account_stock_valuation)
|
||||
self.assertEqual(productA_credit_line.account_id, self.account_production)
|
||||
# one of component move
|
||||
productB_debit_line = self.env['account.move.line'].search([('product_id', '=', self.glass.id), ('credit', '=', 0)])
|
||||
productB_credit_line = self.env['account.move.line'].search([('product_id', '=', self.glass.id), ('debit', '=', 0)])
|
||||
self.assertEqual(productB_debit_line.account_id, self.account_production)
|
||||
self.assertEqual(productB_credit_line.account_id, self.account_stock_valuation)
|
||||
|
||||
# unbuild
|
||||
res_dict = production.button_unbuild()
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.action_validate()
|
||||
Form.from_action(self.env, production.button_unbuild()).save().action_validate()
|
||||
|
||||
# finished product move
|
||||
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('credit', '=', 0)])
|
||||
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('debit', '=', 0)])
|
||||
self.assertEqual(productA_debit_line.account_id, self.stock_input_account)
|
||||
self.assertEqual(productA_credit_line.account_id, self.stock_valuation_account)
|
||||
productA_debit_line = self.env['account.move.line'].search([
|
||||
('product_id', '=', self.dining_table.id),
|
||||
('credit', '=', 0),
|
||||
('name', 'ilike', 'UB'),
|
||||
])
|
||||
productA_credit_line = self.env['account.move.line'].search([
|
||||
('product_id', '=', self.dining_table.id),
|
||||
('debit', '=', 0),
|
||||
('name', 'ilike', 'UB'),
|
||||
])
|
||||
self.assertEqual(productA_debit_line.account_id, self.account_production)
|
||||
self.assertEqual(productA_credit_line.account_id, self.account_stock_valuation)
|
||||
# component move
|
||||
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('credit', '=', 0)])
|
||||
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('debit', '=', 0)])
|
||||
self.assertEqual(productB_debit_line.account_id, self.stock_valuation_account)
|
||||
self.assertEqual(productB_credit_line.account_id, self.stock_output_account)
|
||||
productB_debit_line = self.env['account.move.line'].search([
|
||||
('product_id', '=', self.glass.id),
|
||||
('credit', '=', 0),
|
||||
('name', 'ilike', 'UB'),
|
||||
])
|
||||
productB_credit_line = self.env['account.move.line'].search([
|
||||
('product_id', '=', self.glass.id),
|
||||
('debit', '=', 0),
|
||||
('name', 'ilike', 'UB'),
|
||||
])
|
||||
self.assertEqual(productB_debit_line.account_id, self.account_stock_valuation)
|
||||
self.assertEqual(productB_credit_line.account_id, self.account_production)
|
||||
|
||||
def test_unbuild_account_01(self):
|
||||
"""Test when production location has its valuation accounts. After unbuild,
|
||||
the journal entries are the reversal of the journal entries created when
|
||||
produce the product.
|
||||
def test_mo_overview_comp_different_uom(self):
|
||||
""" Test that the overview takes into account the uom of the component in the price computation
|
||||
"""
|
||||
# set accounts for production location
|
||||
production_location = self.product_A.property_stock_production
|
||||
wip_incoming_account = self.env['account.account'].create({
|
||||
'name': 'wip incoming',
|
||||
'code': '000001',
|
||||
'account_type': 'asset_current',
|
||||
})
|
||||
wip_outgoing_account = self.env['account.account'].create({
|
||||
'name': 'wip outgoing',
|
||||
'code': '000002',
|
||||
'account_type': 'asset_current',
|
||||
})
|
||||
production_location.write({
|
||||
'valuation_in_account_id': wip_incoming_account.id,
|
||||
'valuation_out_account_id': wip_outgoing_account.id,
|
||||
})
|
||||
self.screw.uom_id = self.env.ref('uom.product_uom_pack_6')
|
||||
self.bom_1.bom_line_ids.filtered(lambda l: l.product_id == self.screw).product_uom_id = self.env.ref('uom.product_uom_unit')
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(round(overview_values['data']['summary']['mo_cost'], 2), 677.08, "718.75 - 50 + 50/6")
|
||||
mo.button_mark_done()
|
||||
overview_values = self.env['report.mrp.report_mo_overview'].get_report_values(mo.id)
|
||||
self.assertEqual(round(overview_values['data']['summary']['mo_cost'], 2), 677.08)
|
||||
|
||||
# build
|
||||
production_form = Form(self.env['mrp.production'])
|
||||
production_form.product_id = self.product_A
|
||||
production_form.bom_id = self.bom
|
||||
production_form.product_qty = 1
|
||||
production = production_form.save()
|
||||
production.action_confirm()
|
||||
mo_form = Form(production)
|
||||
mo_form.qty_producing = 1
|
||||
production = mo_form.save()
|
||||
production._post_inventory()
|
||||
def test_mrp_user_without_account_permissions_can_create_bom(self):
|
||||
mrp_user = new_test_user(self.env, 'temp_mrp_user', 'mrp.group_mrp_user')
|
||||
mo_1 = self._create_mo(self.bom_1, 1)
|
||||
mo_1.with_user(mrp_user).button_mark_done()
|
||||
|
||||
|
||||
class TestMrpAccountWorkorder(TestBomPriceOperationCommon):
|
||||
|
||||
def test_01_compute_price_operation_cost(self):
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.dining_table.button_bom_cost()
|
||||
# Total cost of Dining Table = (550) + Total cost of operations (321.25) = 871.25
|
||||
# byproduct have 1%+12% of cost share so the final cost is 757.99
|
||||
self.assertEqual(float_round(self.dining_table.standard_price, precision_digits=2), 757.99)
|
||||
self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost()
|
||||
# Total cost of Dining Table = (718.75) + Total cost of all operations (321.25 + 25.52) = 1065.52
|
||||
# byproduct have 1%+12% of cost share so the final cost is 927
|
||||
self.assertEqual(float_compare(self.dining_table.standard_price, 927, precision_digits=2), 0)
|
||||
|
||||
def test_labor_cost_posting_is_not_rounded_incorrectly(self):
|
||||
""" Test to ensure that labor costs are posted accurately without rounding errors."""
|
||||
# Build
|
||||
self.glass.qty_available = 1
|
||||
self.workcenter.costs_hour = 0.01
|
||||
production = self._create_mo(self.bom_1, 1)
|
||||
production.qty_producing = 1
|
||||
workorder = production.workorder_ids
|
||||
workorder.duration = 30.2
|
||||
workorder.time_ids.write({'duration': 30.2})
|
||||
|
||||
production.move_raw_ids.picked = True
|
||||
production.button_mark_done()
|
||||
|
||||
# finished product move
|
||||
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('credit', '=', 0)])
|
||||
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('debit', '=', 0)])
|
||||
self.assertEqual(productA_debit_line.account_id, self.stock_valuation_account)
|
||||
self.assertEqual(productA_credit_line.account_id, wip_outgoing_account)
|
||||
# component move
|
||||
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('credit', '=', 0)])
|
||||
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('debit', '=', 0)])
|
||||
self.assertEqual(productB_debit_line.account_id, wip_incoming_account)
|
||||
self.assertEqual(productB_credit_line.account_id, self.stock_valuation_account)
|
||||
self.assertEqual(production.workorder_ids.mapped('time_ids').mapped('account_move_line_id').mapped('credit'), [
|
||||
0.06,
|
||||
])
|
||||
|
||||
# unbuild
|
||||
res_dict = production.button_unbuild()
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.action_validate()
|
||||
def test_02_compute_byproduct_price(self):
|
||||
"""Test BoM cost when byproducts with cost share"""
|
||||
|
||||
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('credit', '=', 0)])
|
||||
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('debit', '=', 0)])
|
||||
self.assertEqual(productA_debit_line.account_id, wip_outgoing_account)
|
||||
self.assertEqual(productA_credit_line.account_id, self.stock_valuation_account)
|
||||
# component move
|
||||
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('credit', '=', 0)])
|
||||
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('debit', '=', 0)])
|
||||
self.assertEqual(productB_debit_line.account_id, self.stock_valuation_account)
|
||||
self.assertEqual(productB_credit_line.account_id, wip_incoming_account)
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.assertEqual(self.scrap_wood.standard_price, 30, "Initial price of the By-Product should be 30")
|
||||
# bom price is 871.25. Byproduct cost share is 12%+1% = 13% -> 113.26 for 8+12 units -> 5.66
|
||||
self.scrap_wood.button_bom_cost()
|
||||
self.assertAlmostEqual(self.scrap_wood.standard_price, 5.663125, "After computing price from BoM price should be 20.63")
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestMrpAnalyticAccount(TestMrpCommon):
|
||||
def test_mo_analytic_account(self):
|
||||
def test_wip_accounting_00(self):
|
||||
""" Test that posting a WIP accounting entry works as expected.
|
||||
WIP MO = MO with some time completed on WOs and/or 'consumed' components
|
||||
"""
|
||||
Check that an mrp user without accounting rights is able to mark as done
|
||||
an MO linked to an analytic account.
|
||||
"""
|
||||
if not (self.env.ref('mrp_account_enterprise.account_assembly_hours', raise_if_not_found=False) and self.env.ref('hr_timesheet.group_hr_timesheet_user', raise_if_not_found=False)):
|
||||
self.skipTest("This test requires the installation of hr_timesheet")
|
||||
mrp_user = self.user_mrp_user
|
||||
mrp_user.groups_id = [Command.set([self.ref('mrp.group_mrp_user'), self.ref('hr_timesheet.group_hr_timesheet_user')])]
|
||||
analytic_account = self.env.ref('mrp_account_enterprise.account_assembly_hours')
|
||||
bom = self.bom_4
|
||||
product = bom.product_id
|
||||
bom.bom_line_ids.product_id.standard_price = 1.0
|
||||
bom.analytic_account_id = analytic_account
|
||||
mo = self.env['mrp.production'].with_user(mrp_user.id).create({
|
||||
'product_id': product.id,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'bom_id': bom.id
|
||||
self.glass.qty_available = 2
|
||||
mo = self._create_mo(self.bom_1, 1, confirm=False)
|
||||
|
||||
# post a WIP for an invalid MO, i.e. draft/cancelled/done results in a "Manual Entry"
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': [mo.id]}))
|
||||
wizard.save().confirm()
|
||||
wip_manual_entry1 = self.env['account.move'].search([('ref', 'ilike', 'WIP - Manual Entry')])
|
||||
self.assertEqual(len(wip_manual_entry1), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal")
|
||||
self.assertEqual(wip_manual_entry1[0].wip_production_count, 0, "Non-WIP MOs shouldn't be linked to manual entry")
|
||||
self.assertEqual(len(wip_manual_entry1.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
self.assertRecordValues(wip_manual_entry1.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
])
|
||||
|
||||
# post a WIP for a valid MO - no WO time completed or components consumed => nothing to debit/credit
|
||||
mo.action_confirm()
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': [mo.id]}))
|
||||
wizard.save().confirm()
|
||||
wip_empty_entries = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name)])
|
||||
self.assertEqual(len(wip_empty_entries), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal")
|
||||
self.assertEqual(wip_empty_entries[0].wip_production_count, 1, "WIP MOs should be linked to entries even if no 'done' work")
|
||||
self.assertEqual(len(wip_empty_entries.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
self.assertRecordValues(wip_empty_entries.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
])
|
||||
|
||||
# WO time completed + components consumed
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = mo.product_qty
|
||||
mo = mo_form.save()
|
||||
now = fields.Datetime.now()
|
||||
workorder = mo.workorder_ids[0]
|
||||
self.env['mrp.workcenter.productivity'].create({
|
||||
'workcenter_id': self.workcenter.id,
|
||||
'workorder_id': workorder.id,
|
||||
'date_start': now - timedelta(hours=1),
|
||||
'date_end': now,
|
||||
'loss_id': self.env.ref('mrp.block_reason7').id,
|
||||
})
|
||||
mo.with_user(mrp_user.id).action_confirm()
|
||||
action = mo.with_user(mrp_user.id).button_mark_done()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']).with_user(mrp_user.id)).save()
|
||||
wizard.with_user(mrp_user.id).process()
|
||||
self.assertTrue(mo.move_raw_ids.move_line_ids)
|
||||
self.assertEqual(mo.move_raw_ids.quantity_done, bom.bom_line_ids.product_qty)
|
||||
self.assertEqual(mo.move_raw_ids.state, 'done')
|
||||
workorder.button_finish()
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': [mo.id]}))
|
||||
wizard.save().confirm()
|
||||
wip_entries1 = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name), ('id', 'not in', wip_empty_entries.ids)])
|
||||
self.assertEqual(len(wip_entries1), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal")
|
||||
self.assertEqual(wip_entries1[0].wip_production_count, 1, "WIP MOs should be linked to entry")
|
||||
self.assertEqual(len(wip_entries1.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
total_component_price = sum(m.product_qty * m.product_id.standard_price for m in mo.move_raw_ids)
|
||||
self.assertRecordValues(wip_entries1.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': total_component_price, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 100.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 100.0 + total_component_price},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': total_component_price},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 100.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 100.0 + total_component_price, 'credit': 0.0},
|
||||
])
|
||||
|
||||
# Multi-records case
|
||||
previous_wip_ids = wip_entries1.ids + wip_empty_entries.ids
|
||||
mo_2 = self._create_mo(self.bom_1, 1, confirm=False)
|
||||
mos = (mo | mo_2)
|
||||
|
||||
# Draft MOs should be ignored when selecting multiple MOs
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': mos.ids}))
|
||||
wizard.save().confirm()
|
||||
wip_entries2 = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name), ('id', 'not in', previous_wip_ids)])
|
||||
self.assertEqual(len(wip_entries2), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal, for 1 MO")
|
||||
self.assertTrue(mo_2.name not in wip_entries2[0].ref, "Draft MO should be completely disregarded by wizard")
|
||||
self.assertEqual(wip_entries2[0].wip_production_count, 1, "Only WIP MOs should be linked to entry")
|
||||
self.assertEqual(len(wip_entries2.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
self.assertRecordValues(wip_entries2.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': total_component_price, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 100.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 100.0 + total_component_price},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': total_component_price},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 100.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 100.0 + total_component_price, 'credit': 0.0},
|
||||
])
|
||||
previous_wip_ids += wip_entries2.ids
|
||||
|
||||
# MOs' WIP amounts should be aggregated
|
||||
mo_2.action_confirm()
|
||||
mo_2.qty_producing = mo_2.product_qty
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': mos.ids}))
|
||||
wizard.save().confirm()
|
||||
wip_entries3 = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name), ('id', 'not in', previous_wip_ids)])
|
||||
self.assertEqual(len(wip_entries3), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal, for both MOs")
|
||||
self.assertTrue(mo_2.name in wip_entries3[0].ref, "Both MOs should have been considered")
|
||||
self.assertEqual(wip_entries3[0].wip_production_count, 2, "Both WIP MOs should be linked to entry")
|
||||
self.assertEqual(len(wip_entries3.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
total_component_price = sum(m.quantity * m.product_id.standard_price for m in mo_2.move_raw_ids)
|
||||
self.assertRecordValues(wip_entries3.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': total_component_price, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 100.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 100.0 + total_component_price},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': total_component_price},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 100.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 100.0 + total_component_price, 'credit': 0.0},
|
||||
])
|
||||
previous_wip_ids += wip_entries3.ids
|
||||
|
||||
# Done MO should be ignored
|
||||
mo_2.move_raw_ids.picked = True
|
||||
mo_2.button_mark_done()
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': mos.ids}))
|
||||
wizard.save().confirm()
|
||||
wip_entries4 = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name), ('id', 'not in', previous_wip_ids)])
|
||||
self.assertEqual(len(wip_entries4), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal, for 1 MO")
|
||||
self.assertTrue(mo_2.name not in wip_entries4[0].ref, "Done MO should be completely disregarded by wizard")
|
||||
self.assertEqual(wip_entries4[0].wip_production_count, 1, "Only WIP MOs should be linked to entry")
|
||||
self.assertEqual(len(wip_entries4.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
total_component_price = sum(m.product_qty * m.product_id.standard_price for m in mo.move_raw_ids)
|
||||
self.assertRecordValues(wip_entries4.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': total_component_price, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 100.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 100.0 + total_component_price},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': total_component_price},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 100.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 100.0 + total_component_price, 'credit': 0.0},
|
||||
])
|
||||
previous_wip_ids += wip_entries4.ids
|
||||
|
||||
# WO time completed + components consumed, but WIP date is for before they were done => nothing to debit/credit
|
||||
wizard = Form(self.env['mrp.account.wip.accounting'].with_context({'active_ids': [mo.id]}))
|
||||
wizard.date = now - timedelta(days=2)
|
||||
wizard.save().confirm()
|
||||
wip_entries5 = self.env['account.move'].search([('ref', 'ilike', 'WIP - ' + mo.name), ('id', 'not in', previous_wip_ids)])
|
||||
self.assertEqual(len(wip_entries5), 2, "Should be 2 journal entries: 1 for the WIP accounting + 1 for its reversal")
|
||||
self.assertEqual(wip_entries5[0].wip_production_count, 1, "WIP MOs should be linked to entry")
|
||||
self.assertEqual(len(wip_entries5.line_ids), 6, "Should be 3 lines per journal entry: 1 for 'Component Value', 1 for '(WO) overhead', 1 for WIP")
|
||||
self.assertRecordValues(wip_entries5.line_ids, [
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.account_stock_valuation.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_overhead_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
{'account_id': self.env.company.account_production_wip_account_id.id, 'debit': 0.0, 'credit': 0.0},
|
||||
])
|
||||
|
||||
def test_labor_cost_balancing(self):
|
||||
""" When the workcenter_cost ends up like x.xx5 with a currency rounding of 0.01,
|
||||
the valuation 'account.move' was unbalanced.
|
||||
This happened because the credit value rounded up twice, and instead of having -0.005 + 0.005 => 0,
|
||||
we had -0 + 0.01 => +0.01, which made the credit differ from the debit by 0.01.
|
||||
This test ensures that if the workcenter_cost is rounded to 0.01, then the credit value is correctly
|
||||
decremented by 0.01.
|
||||
"""
|
||||
# Build
|
||||
self.dining_table.categ_id = self.category_avco_auto
|
||||
self.glass.qty_available = 1
|
||||
production = self._create_mo(self.bom_1, 1)
|
||||
production.qty_producing = 1
|
||||
workorder = production.workorder_ids
|
||||
workorder.duration = 0.03 # ~= 2 seconds (1.8 seconds exactly)
|
||||
workorder.time_ids.write({'duration': 0.03}) # Ensure that the duration is correct
|
||||
self.assertEqual(workorder._cal_cost(), (len(workorder) * 2 / 3600) * 100) # 2 seconds at $100/h
|
||||
|
||||
production.move_raw_ids.picked = True
|
||||
production.button_mark_done()
|
||||
|
||||
# value = finished product value + labour cost - byproduct cost share
|
||||
account_move = production.move_finished_ids.account_move_id
|
||||
self.assertRecordValues(account_move.line_ids, [
|
||||
{'credit': 625.6, 'debit': 0.00}, # Credit Line
|
||||
{'credit': 0.00, 'debit': 625.6}, # Debit Line
|
||||
])
|
||||
labour_move = workorder.time_ids.account_move_line_id.move_id
|
||||
# sum of workorder costs but rounded at company currency
|
||||
self.assertRecordValues(labour_move.line_ids, [
|
||||
{'credit': 0.36, 'debit': 0.00},
|
||||
{'credit': 0.00, 'debit': 0.36},
|
||||
])
|
||||
|
||||
def test_labor_cost_over_consumption(self):
|
||||
""" Test the labour accounting entries creation is independent of consumption variation"""
|
||||
self.glass.qty_available = 1
|
||||
production = self._create_mo(self.bom_1, 1)
|
||||
production.workorder_ids.duration = 60
|
||||
|
||||
production.qty_producing = 1
|
||||
|
||||
# overconsume one component to get a warning wizard
|
||||
production.move_raw_ids[0].quantity += 2
|
||||
|
||||
production.move_raw_ids.picked = True
|
||||
action = production.button_mark_done()
|
||||
consumption_warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])).save()
|
||||
consumption_warning.action_confirm()
|
||||
|
||||
mo_aml = self.env['account.move.line'].search([('name', 'like', production.name)])
|
||||
labour = 600
|
||||
compo_price = 718.75 + 2 * 200
|
||||
cost_share = 0.13
|
||||
self.assertEqual(len(mo_aml), 6, "2 Labour + 2 finished product + 7 for the components")
|
||||
self.assertRecordValues(mo_aml, [
|
||||
{'name': production.name + ' - Labour', 'debit': 0.0, 'credit': labour},
|
||||
{'name': production.name + ' - Labour', 'debit': labour, 'credit': 0.0},
|
||||
{'name': production.name + ' - ' + self.dining_table.name, 'debit': 0.0, 'credit': (labour + compo_price) * (1 - cost_share)},
|
||||
{'name': production.name + ' - ' + self.dining_table.name, 'debit': (labour + compo_price) * (1 - cost_share), 'credit': 0.0},
|
||||
{'name': production.name + ' - ' + self.glass.name, 'debit': 0.0, 'credit': 100.0},
|
||||
{'name': production.name + ' - ' + self.glass.name, 'debit': 100.0, 'credit': 0.0},
|
||||
])
|
||||
|
||||
def test_estimated_cost_valuation(self):
|
||||
""" Test that operations with 'estimated' cost correctly compute the cost.
|
||||
The cost should be equal to workcenter.costs_hour * workorder.duration_expected. """
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
self.bom_1.operation_ids.cost_mode = 'actual'
|
||||
mo.workorder_ids.duration = 60
|
||||
self.assertEqual(mo.workorder_ids._cal_cost(), 600)
|
||||
|
||||
# Cost should stay the same for a done MO if nothing else is changed
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.workcenter.costs_hour = 333
|
||||
self.assertEqual(mo.workorder_ids._cal_cost(), 600)
|
||||
|
||||
def test_mo_without_finished_moves(self):
|
||||
"""Test that a MO without finished moves can post inventory and be completed."""
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
workorder = mo.workorder_ids
|
||||
workorder.duration = 60
|
||||
self.assertEqual(workorder._cal_cost(), 600)
|
||||
# Simulate missing finished moves
|
||||
mo.move_finished_ids.unlink()
|
||||
# Post inventory and complete MO
|
||||
mo.move_raw_ids.picked = True
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
|
|
|||
|
|
@ -1,356 +1,263 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """
|
||||
|
||||
from odoo.addons.stock_account.tests.test_stockvaluationlayer import TestStockValuationCommon
|
||||
from odoo.addons.mrp_account.tests.common import TestBomPriceCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestMrpValuationCommon(TestStockValuationCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpValuationCommon, cls).setUpClass()
|
||||
cls.component_category = cls.env['product.category'].create(
|
||||
{'name': 'category2'}
|
||||
)
|
||||
cls.component = cls.env['product.product'].create({
|
||||
'name': 'component1',
|
||||
'type': 'product',
|
||||
'categ_id': cls.component_category.id,
|
||||
})
|
||||
cls.bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product1.id,
|
||||
'product_tmpl_id': cls.product1.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.component.id, 'product_qty': 1})
|
||||
]})
|
||||
|
||||
def _make_mo(self, bom, quantity=1):
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = bom.product_id
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = quantity
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
return mo
|
||||
|
||||
def _produce(self, mo, quantity=0):
|
||||
mo_form = Form(mo)
|
||||
if not quantity:
|
||||
quantity = mo.product_qty - mo.qty_produced
|
||||
mo_form.qty_producing += quantity
|
||||
mo = mo_form.save()
|
||||
PRICE = 718.75 - 100 # total price minus glass
|
||||
|
||||
|
||||
class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
class TestMrpValuationStandard(TestBomPriceCommon):
|
||||
def _get_production_cost_move_lines(self):
|
||||
return self.env['account.move.line'].search([
|
||||
('account_id', '=', self.account_production.id),
|
||||
], order='date, id')
|
||||
|
||||
def test_fifo_fifo_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.dining_table.categ_id = self.category_fifo
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 10)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
mo = mo.production_group_id.production_ids[-1]
|
||||
self.assertEqual(self.glass.total_value, 20)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE + 10)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)
|
||||
|
||||
def test_fifo_fifo_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.glass.categ_id = self.category_fifo
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
|
||||
def test_fifo_byproduct(self):
|
||||
""" Check that a MO byproduct with a cost share calculates correct svl """
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
|
||||
# add byproduct
|
||||
byproduct_cost_share = 10
|
||||
byproduct = self.env['product.product'].create({
|
||||
'name': 'byproduct',
|
||||
'type': 'product',
|
||||
'categ_id': self.product1.product_tmpl_id.categ_id.id,
|
||||
})
|
||||
self.bom.write({
|
||||
'byproduct_ids': [(0, 0, {'product_id': byproduct.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 1, 'cost_share': byproduct_cost_share})]
|
||||
})
|
||||
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 10 * (100 - byproduct_cost_share) / 100)
|
||||
self.assertEqual(byproduct.value_svl, 10 * byproduct_cost_share / 100)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
self.assertEqual(byproduct.quantity_svl, 1)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30 * (100 - byproduct_cost_share) / 100)
|
||||
self.assertEqual(byproduct.value_svl, 30 * byproduct_cost_share / 100)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(byproduct.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, (2 * PRICE + 10 + 20) / 2)
|
||||
|
||||
def test_fifo_unbuild(self):
|
||||
""" This test creates an MO and then creates an unbuild
|
||||
orders and checks the stock valuation.
|
||||
"""
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.glass.categ_id = self.category_fifo
|
||||
# ---------------------------------------------------
|
||||
# MO
|
||||
# ---------------------------------------------------
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 1)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.glass.total_value, 20)
|
||||
# ---------------------------------------------------
|
||||
# Unbuild
|
||||
# ---------------------------------------------------
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.save().action_unbuild()
|
||||
self.assertEqual(self.component.value_svl, 30)
|
||||
self.assertEqual(self.glass.total_value, 30)
|
||||
|
||||
def test_fifo_produce_deliver_return_unbuild(self):
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
|
||||
out_move = self._make_out_move(self.dining_table, 1.0, create_picking=True)
|
||||
self._make_return(out_move, 1.0)
|
||||
|
||||
unbuild_form = Form(self.env['mrp.unbuild'])
|
||||
unbuild_form.mo_id = mo
|
||||
unbuild_form.save().action_unbuild()
|
||||
|
||||
moves = self.env['stock.move'].search([('product_id', '=', self.dining_table.id)])
|
||||
self.assertRecordValues(moves, [
|
||||
{'value': PRICE + 10, 'quantity': 1.0, 'is_in': True, 'remaining_value': 0.0, 'remaining_qty': 0.0},
|
||||
{'value': PRICE + 10, 'quantity': 1.0, 'is_in': False, 'remaining_value': 0.0, 'remaining_qty': 0.0},
|
||||
{'value': PRICE + 10, 'quantity': 1.0, 'is_in': True, 'remaining_value': 0.0, 'remaining_qty': 0.0},
|
||||
{'value': PRICE + 10, 'quantity': 1.0, 'is_in': False, 'remaining_value': 0.0, 'remaining_qty': 0.0},
|
||||
])
|
||||
|
||||
def test_fifo_avco_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 10)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
mo = mo.production_group_id.production_ids[-1]
|
||||
self.assertEqual(self.glass.total_value, 20)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE + 10)
|
||||
self._produce(mo)
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)
|
||||
|
||||
def test_fifo_avco_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
self.dining_table.categ_id = self.category_fifo
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE * 2 + 10 + 20)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, (PRICE * 2 + 10 + 20) / 2)
|
||||
|
||||
def test_fifo_std_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.standard_price = 8.8
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.dining_table.categ_id = self.category_standard
|
||||
self.dining_table.standard_price = 8.8
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
self.assertEqual(self.glass.total_value, 20)
|
||||
self.assertEqual(self.dining_table.total_value, 8.8)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 8.8 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 8.8 * 2)
|
||||
|
||||
def test_fifo_std_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.standard_price = 8.8
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.dining_table.categ_id = self.category_standard
|
||||
self.dining_table.standard_price = 8.8
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 8.8 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 8.8 * 2)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, 8.8)
|
||||
|
||||
def test_std_avco_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.component.standard_price = 8.8
|
||||
self.glass.categ_id = self.category_standard
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
|
||||
self._make_in_move(self.component, 1)
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1)
|
||||
self._make_in_move(self.glass, 1)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
self.assertEqual(self.glass.total_value, 100)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE + 100)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 8.8 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * (PRICE + 100))
|
||||
|
||||
def test_std_avco_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.component.standard_price = 8.8
|
||||
self.glass.categ_id = self.category_standard
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
|
||||
self._make_in_move(self.component, 1)
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1)
|
||||
self._make_in_move(self.glass, 1)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 8.8 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.product1.standard_price, 8.8)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * (PRICE + 100))
|
||||
self.assertEqual(self.dining_table.standard_price, PRICE + 100)
|
||||
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE + 100)
|
||||
|
||||
# Update component price
|
||||
self.component.standard_price = 0
|
||||
self.glass.standard_price = 0
|
||||
|
||||
self._make_in_move(self.component, 3)
|
||||
mo = self._make_mo(self.bom, 3)
|
||||
self._make_in_move(self.glass, 3)
|
||||
mo = self._create_mo(self.bom_1, 3)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.quantity_svl, 4)
|
||||
self.assertEqual(self.product1.standard_price, 2.2)
|
||||
self.assertEqual(self.dining_table.total_value, 4 * PRICE + 100)
|
||||
self.assertEqual(self.dining_table.standard_price, (4 * PRICE + 100) / 4)
|
||||
|
||||
def test_std_std_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.component.standard_price = 8.8
|
||||
self.product1.standard_price = 7.2
|
||||
self.glass.categ_id = self.category_standard
|
||||
self.dining_table.categ_id = self.category_standard
|
||||
|
||||
self._make_in_move(self.component, 1)
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1)
|
||||
self._make_in_move(self.glass, 1)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 7.2)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
self.assertEqual(self.glass.total_value, 100)
|
||||
self.assertEqual(self.dining_table.total_value, 1000)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 7.2 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2000)
|
||||
|
||||
def test_std_std_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
|
||||
self.component.standard_price = 8.8
|
||||
self.product1.standard_price = 7.2
|
||||
self.glass.categ_id = self.category_standard
|
||||
self.dining_table.categ_id = self.category_standard
|
||||
|
||||
self._make_in_move(self.component, 1)
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1)
|
||||
self._make_in_move(self.glass, 1)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 7.2 * 2)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 7.2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2000)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, 1000)
|
||||
|
||||
def test_avco_avco_1(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.glass.categ_id = self.category_avco
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 15)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 1)
|
||||
self.assertEqual(self.glass.total_value, 15)
|
||||
self.assertEqual(self.dining_table.total_value, PRICE + 15)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * PRICE + 30)
|
||||
|
||||
def test_avco_avco_2(self):
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.glass.categ_id = self.category_avco
|
||||
self.dining_table.categ_id = self.category_avco
|
||||
|
||||
self._make_in_move(self.component, 1, 10)
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.component.value_svl, 0)
|
||||
self.assertEqual(self.product1.value_svl, 30)
|
||||
self.assertEqual(self.component.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self._make_out_move(self.product1, 1)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, 2 * PRICE + 30)
|
||||
self._make_out_move(self.dining_table, 1)
|
||||
self.assertEqual(self.dining_table.total_value, (2 * PRICE + 30) / 2)
|
||||
|
||||
def test_validate_draft_kit(self):
|
||||
"""
|
||||
|
|
@ -358,28 +265,61 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
|||
validate it. From client side, such a behaviour is possible with
|
||||
the Barcode app.
|
||||
"""
|
||||
self.component.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
self.product1.type = 'consu'
|
||||
self.bom.type = 'phantom'
|
||||
self.component.standard_price = 1424
|
||||
self.plywood_sheet.qty_available = 0
|
||||
self.plywood_sheet.categ_id = self.category_avco
|
||||
|
||||
receipt = self.env['stock.picking'].create({
|
||||
'location_id': self.customer_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'state': 'draft',
|
||||
'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_id': self.product1.uom_id.id,
|
||||
'product_id': self.table_head.id,
|
||||
'quantity': 12,
|
||||
'product_uom_id': self.table_head.uom_id.id,
|
||||
'location_id': self.customer_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
})]
|
||||
})],
|
||||
})
|
||||
receipt.move_ids.picked = True
|
||||
receipt.button_validate()
|
||||
|
||||
self.assertEqual(receipt.state, 'done')
|
||||
self.assertRecordValues(receipt.move_ids, [
|
||||
{'product_id': self.component.id, 'quantity_done': 1, 'state': 'done'},
|
||||
])
|
||||
self.assertEqual(self.component.qty_available, 1)
|
||||
self.assertEqual(self.component.value_svl, 1424)
|
||||
self.assertEqual(self.plywood_sheet.qty_available, 12)
|
||||
self.assertEqual(self.plywood_sheet.total_value, 2400)
|
||||
|
||||
def test_production_account_00(self):
|
||||
"""Create move into/out of a production location, test we create account
|
||||
entries with the Production Cost account.
|
||||
"""
|
||||
self.dining_table.categ_id.property_cost_method = 'standard'
|
||||
|
||||
# move into production location
|
||||
self._make_out_move(self.dining_table, 1, location_dest_id=self.prod_location.id)
|
||||
|
||||
in_aml = self._get_production_cost_move_lines()
|
||||
self.assertEqual(in_aml.debit, 1000)
|
||||
self.assertEqual(in_aml.product_id, self.dining_table)
|
||||
|
||||
# move out of production location
|
||||
self._make_in_move(self.dining_table, 1, location_id=self.prod_location.id)
|
||||
|
||||
out_aml = self._get_production_cost_move_lines() - in_aml
|
||||
self.assertEqual(out_aml.credit, 1000)
|
||||
self.assertEqual(in_aml.product_id, self.dining_table)
|
||||
|
||||
def test_average_cost_unbuild_component_change_move_qty(self):
|
||||
"""
|
||||
Ensures that we can modify the quantity on the stock move of the components after an unbuild
|
||||
"""
|
||||
mo = self._create_mo(self.bom_1, 1)
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
action = mo.button_unbuild()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
wizard.product_qty = 1
|
||||
unbuild = wizard.save()
|
||||
unbuild.action_validate()
|
||||
# check that changing the quantity on the move form does not create an error
|
||||
comp_move = mo.unbuild_ids.produce_line_ids.filtered(lambda move: move.product_id.id == self.glass.id)
|
||||
with Form(comp_move.move_line_ids[0]) as form:
|
||||
form.quantity = 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """
|
||||
|
||||
from odoo.addons.mrp_account.tests.common import TestBomPriceOperationCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
PRICE = 718.75 + 2 * 321.25 - 100 # component price + operations - glass cost
|
||||
|
||||
|
||||
class TestMrpValuationOperationStandard(TestBomPriceOperationCommon):
|
||||
|
||||
def test_fifo_byproduct(self):
|
||||
""" Check that a MO byproduct with a cost share calculates correct svl """
|
||||
self.glass.categ_id = self.category_fifo
|
||||
self.glass.qty_available = 0
|
||||
self.scrap_wood.categ_id = self.category_avco
|
||||
byproduct_cost_share = 0.13
|
||||
|
||||
self._make_in_move(self.glass, 1, 10)
|
||||
self._make_in_move(self.glass, 1, 20)
|
||||
|
||||
mo = self._create_mo(self.bom_1, 2)
|
||||
self._produce(mo, 1)
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.production_group_id.production_ids[-1]
|
||||
self.assertEqual(self.glass.total_value, 20)
|
||||
self.assertEqual(self.dining_table.total_value, self.company.currency_id.round((PRICE + 10) * (1 - byproduct_cost_share)))
|
||||
self.assertEqual(self.scrap_wood.total_value, self.company.currency_id.round((PRICE + 10) * byproduct_cost_share))
|
||||
self._produce(mo)
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(self.glass.total_value, 0)
|
||||
self.assertEqual(self.dining_table.total_value, self.company.currency_id.round((2 * PRICE + 30) * (1 - byproduct_cost_share)))
|
||||
moves = self.env['stock.move'].search([
|
||||
('product_id', '=', self.scrap_wood.id),
|
||||
])
|
||||
self.assertRecordValues(moves, [
|
||||
{'value': self.company.currency_id.round((PRICE + 10) * 0.01)},
|
||||
{'value': self.company.currency_id.round((PRICE + 10) * 0.12)},
|
||||
{'value': self.company.currency_id.round((PRICE + 20) * 0.01)},
|
||||
{'value': self.company.currency_id.round((PRICE + 20) * 0.12)},
|
||||
])
|
||||
|
||||
# def test_average_cost_unbuild_with_byproducts(self):
|
||||
# """ Ensures that an unbuild for a manufacturing order using avg cost products won't copy
|
||||
# the value of the main product for every byproduct line, regardless of their real value.
|
||||
# """
|
||||
# self.dining_table.categ_id = self.category_avco
|
||||
# self.glass.categ_id = self.category_avco
|
||||
# self.scrap_wood.categ_id = self.category_avco
|
||||
# byproduct_cost_share = 0.13
|
||||
#
|
||||
# self._make_in_move(self.glass, 10)
|
||||
# production = self._create_mo(self.bom_1, 1)
|
||||
# self._produce(production)
|
||||
# production.button_mark_done()
|
||||
#
|
||||
# self.assertEqual(self.scrap_wood.total_value, (PRICE + 10) * byproduct_cost_share)
|
||||
# self.assertRecordValues(production.move_finished_ids, [
|
||||
# {'product_id': self.dining_table.id, 'value': (PRICE + 10) * (1 - byproduct_cost_share)},
|
||||
# {'product_id': self.scrap_wood.id, 'value': (PRICE + 10) * 0.12},
|
||||
# {'product_id': self.scrap_wood.id, 'value': (PRICE + 10) * 0.1},
|
||||
# ])
|
||||
#
|
||||
# action = production.button_unbuild()
|
||||
# wizard = Form(self.env[action['res_model']].with_context(action['context']))
|
||||
# wizard.product_qty = 1
|
||||
# unbuild = wizard.save()
|
||||
# unbuild.action_validate()
|
||||
#
|
||||
# unbuild_move = self.env['stock.move'].search([('reference', '=', unbuild.name)])
|
||||
# self.assertRecordValues(unbuild_move, [
|
||||
# {'product_id': self.dining_table.id, 'value': (PRICE + 10) * (1 - byproduct_cost_share)},
|
||||
# {'product_id': self.scrap_wood.id, 'value': (PRICE + 10) * byproduct_cost_share},
|
||||
# {'product_id': self.glass.id, 'value': 10},
|
||||
# ])
|
||||
Loading…
Add table
Add a link
Reference in a new issue