257 lines
11 KiB
Python
257 lines
11 KiB
Python
# -*- 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
|
|
|
|
|
|
class TestBomPriceCommon(common.TransactionCase):
|
|
|
|
@classmethod
|
|
def _create_product(cls, name, price):
|
|
return cls.Product.create({
|
|
'name': name,
|
|
'type': 'product',
|
|
'standard_price': price,
|
|
})
|
|
|
|
@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')
|
|
# Required for `product_id ` to be visible in the view
|
|
cls.env.user.groups_id += cls.env.ref('product.group_product_variant')
|
|
cls.Product = cls.env['product.product']
|
|
cls.Bom = cls.env['mrp.bom']
|
|
|
|
# Products.
|
|
cls.dining_table = cls._create_product('Dining Table', 1000)
|
|
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)
|
|
|
|
# Unit of Measure.
|
|
cls.unit = cls.env.ref("uom.product_uom_unit")
|
|
cls.dozen = cls.env.ref("uom.product_uom_dozen")
|
|
|
|
# Bills Of Materials.
|
|
# -------------------------------------------------------------------------------
|
|
# Cost of BoM (Dining Table 1 Unit)
|
|
# Component Cost = Table Head 1 Unit * 300 = 300 (468.75 from it's components)
|
|
# Screw 5 Unit * 10 = 50
|
|
# Leg 4 Unit * 25 = 100
|
|
# Glass 1 Unit * 100 = 100
|
|
# Total = 550 [718.75 if components of Table Head considered] (for 1 Unit)
|
|
# -------------------------------------------------------------------------------
|
|
|
|
bom_form = Form(cls.Bom)
|
|
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.type = 'normal'
|
|
with bom_form.bom_line_ids.new() as line:
|
|
line.product_id = cls.table_head
|
|
line.product_qty = 1
|
|
with bom_form.bom_line_ids.new() as line:
|
|
line.product_id = cls.screw
|
|
line.product_qty = 5
|
|
with bom_form.bom_line_ids.new() as line:
|
|
line.product_id = cls.leg
|
|
line.product_qty = 4
|
|
with bom_form.bom_line_ids.new() as line:
|
|
line.product_id = cls.glass
|
|
line.product_qty = 1
|
|
cls.bom_1 = bom_form.save()
|
|
|
|
# Table Head's components.
|
|
cls.plywood_sheet = cls._create_product('Plywood Sheet', 200)
|
|
cls.bolt = cls._create_product('Bolt', 10)
|
|
cls.colour = cls._create_product('Colour', 100)
|
|
cls.corner_slide = cls._create_product('Corner Slide', 25)
|
|
|
|
# -----------------------------------------------------------------
|
|
# Cost of BoM (Table Head 1 Dozen)
|
|
# Component Cost = Plywood Sheet 12 Unit * 200 = 2400
|
|
# Bolt 60 Unit * 10 = 600
|
|
# Colour 12 Unit * 100 = 1200
|
|
# Corner Slide 57 Unit * 25 = 1425
|
|
# Total = 5625
|
|
# 1 Unit price (5625/12) = 468.75
|
|
# -----------------------------------------------------------------
|
|
|
|
bom_form2 = Form(cls.Bom)
|
|
bom_form2.product_id = cls.table_head
|
|
bom_form2.product_tmpl_id = cls.table_head.product_tmpl_id
|
|
bom_form2.product_qty = 1.0
|
|
bom_form2.product_uom_id = cls.dozen
|
|
bom_form2.type = 'phantom'
|
|
with bom_form2.bom_line_ids.new() as line:
|
|
line.product_id = cls.plywood_sheet
|
|
line.product_qty = 12
|
|
with bom_form2.bom_line_ids.new() as line:
|
|
line.product_id = cls.bolt
|
|
line.product_qty = 60
|
|
with bom_form2.bom_line_ids.new() as line:
|
|
line.product_id = cls.colour
|
|
line.product_qty = 12
|
|
with bom_form2.bom_line_ids.new() as line:
|
|
line.product_id = cls.corner_slide
|
|
line.product_qty = 57
|
|
cls.bom_2 = bom_form2.save()
|
|
|
|
|
|
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")
|
|
|
|
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,
|
|
})
|
|
|
|
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)
|
|
# -----------------------------------------------------------------
|
|
# Operation cost calculate for 1 units
|
|
# Cutting (15 + 15 + (20 * 100/80) / 60) * 100 = 91.67
|
|
# Drilling (15 + 15 + (25 * 100/80) / 60) * 100 = 102.08
|
|
# Fitting (15 + 15 + (30 * 100/80) / 60) * 100 = 112.50
|
|
# Table Capacity (3 operations * (2 + 1) / 60) * 100 = 15.00
|
|
# ----------------------------------------
|
|
# Operation Cost 1 unit = 321.25
|
|
# -----------------------------------------------------------------
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Table Head Operation Cost (1 Dozen)
|
|
# --------------------------------------------------------------------------
|
|
# Operation cost calculate for 1 dozens
|
|
# Cutting (15 + 15 + (20 * 1 * 100/80) / 60) * 100 = 91.67
|
|
# Drilling (15 + 15 + (25 * 1 * 100/80) / 60) * 100 = 102.08
|
|
# Fitting (15 + 15 + (30 * 1 * 100/80) / 60) * 100 = 112.50
|
|
# Table Capacity (3 operations * (2 + 1) / 60) * 100 = 15.00
|
|
# ----------------------------------------
|
|
# 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': [
|
|
(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,
|
|
}),
|
|
(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,
|
|
}),
|
|
],
|
|
}),
|
|
|
|
# Cost Breakdown.
|
|
# -------------------------------------------------------------------------------
|
|
# Total Cost of BoM = 550 [718.75 if components of Table Head considered] (for 1 Unit)
|
|
# Dining Table 1 Unit = 1 - (25 + 50) / 100 * 550 = 0.25 * 550 = 137.5
|
|
# 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")
|