summaryrefslogtreecommitdiff
path: root/addons/mrp/tests/test_bom.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp/tests/test_bom.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mrp/tests/test_bom.py')
-rw-r--r--addons/mrp/tests/test_bom.py832
1 files changed, 832 insertions, 0 deletions
diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py
new file mode 100644
index 00000000..bfa47231
--- /dev/null
+++ b/addons/mrp/tests/test_bom.py
@@ -0,0 +1,832 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import exceptions
+from odoo.tests import Form
+from odoo.addons.mrp.tests.common import TestMrpCommon
+from odoo.tools import float_compare, float_round
+
+
+class TestBoM(TestMrpCommon):
+
+ def test_01_explode(self):
+ boms, lines = self.bom_1.explode(self.product_4, 3)
+ self.assertEqual(set([bom[0].id for bom in boms]), set(self.bom_1.ids))
+ self.assertEqual(set([line[0].id for line in lines]), set(self.bom_1.bom_line_ids.ids))
+
+ boms, lines = self.bom_3.explode(self.product_6, 3)
+ self.assertEqual(set([bom[0].id for bom in boms]), set((self.bom_2 | self.bom_3).ids))
+ self.assertEqual(
+ set([line[0].id for line in lines]),
+ set((self.bom_2 | self.bom_3).mapped('bom_line_ids').filtered(lambda line: not line.child_bom_id or line.child_bom_id.type != 'phantom').ids))
+
+ def test_10_variants(self):
+ test_bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'normal',
+ })
+ test_bom.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
+ ],
+ })
+ test_bom_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ })
+ test_bom_l2 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_3.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
+ })
+ test_bom_l3 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_4.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
+ })
+ boms, lines = test_bom.explode(self.product_7_3, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l2, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l3, [l[0] for l in lines])
+
+ boms, lines = test_bom.explode(self.product_7_1, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertIn(test_bom_l2, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l3, [l[0] for l in lines])
+
+ boms, lines = test_bom.explode(self.product_7_2, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l2, [l[0] for l in lines])
+ self.assertIn(test_bom_l3, [l[0] for l in lines])
+
+ def test_11_multi_level_variants(self):
+ tmp_picking_type = self.env['stock.picking.type'].create({
+ 'name': 'Manufacturing',
+ 'code': 'mrp_operation',
+ 'sequence_code': 'TMP',
+ 'sequence_id': self.env['ir.sequence'].create({
+ 'code': 'mrp.production',
+ 'name': 'tmp_production_sequence',
+ }).id,
+ })
+ test_bom_1 = self.env['mrp.bom'].create({
+ 'product_tmpl_id': self.product_5.product_tmpl_id.id,
+ 'product_uom_id': self.product_5.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'
+ })
+ test_bom_1.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
+ ],
+ })
+ test_bom_1_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_1.id,
+ 'product_id': self.product_3.id,
+ 'product_qty': 3,
+ })
+
+ test_bom_2 = self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'normal',
+ })
+ test_bom_2.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
+ ]
+ })
+ test_bom_2_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ })
+ test_bom_2_l2 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_5.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
+ })
+ test_bom_2_l3 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_5.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
+ })
+ test_bom_2_l4 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_4.id,
+ 'product_qty': 2,
+ })
+
+ # check product > product_tmpl
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ # check sequence priority
+ test_bom_1.write({'sequence': 1})
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | test_bom_1).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | test_bom_1.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ # check with another picking_type
+ test_bom_1.write({'picking_type_id': self.warehouse_1.manu_type_id.id})
+ self.bom_2.write({'picking_type_id': tmp_picking_type.id})
+ test_bom_2.write({'picking_type_id': tmp_picking_type.id})
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ #check recursion
+ test_bom_3 = self.env['mrp.bom'].create({
+ 'product_id': self.product_9.id,
+ 'product_tmpl_id': self.product_9.product_tmpl_id.id,
+ 'product_uom_id': self.product_9.uom_id.id,
+ 'product_qty': 1.0,
+ 'consumption': 'flexible',
+ 'type': 'normal'
+ })
+ test_bom_4 = self.env['mrp.bom'].create({
+ 'product_id': self.product_10.id,
+ 'product_tmpl_id': self.product_10.product_tmpl_id.id,
+ 'product_uom_id': self.product_10.uom_id.id,
+ 'product_qty': 1.0,
+ 'consumption': 'flexible',
+ 'type': 'phantom'
+ })
+ test_bom_3_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_3.id,
+ 'product_id': self.product_10.id,
+ 'product_qty': 1.0,
+ })
+ test_bom_4_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_4.id,
+ 'product_id': self.product_9.id,
+ 'product_qty': 1.0,
+ })
+ with self.assertRaises(exceptions.UserError):
+ test_bom_3.explode(self.product_9, 1)
+
+ def test_12_multi_level_variants2(self):
+ """Test skip bom line with same attribute values in bom lines."""
+
+ Product = self.env['product.product']
+ ProductAttribute = self.env['product.attribute']
+ ProductAttributeValue = self.env['product.attribute.value']
+
+ # Product Attribute
+ att_color = ProductAttribute.create({'name': 'Color', 'sequence': 1})
+ att_size = ProductAttribute.create({'name': 'size', 'sequence': 2})
+
+ # Product Attribute color Value
+ att_color_red = ProductAttributeValue.create({'name': 'red', 'attribute_id': att_color.id, 'sequence': 1})
+ att_color_blue = ProductAttributeValue.create({'name': 'blue', 'attribute_id': att_color.id, 'sequence': 2})
+ # Product Attribute size Value
+ att_size_big = ProductAttributeValue.create({'name': 'big', 'attribute_id': att_size.id, 'sequence': 1})
+ att_size_medium = ProductAttributeValue.create({'name': 'medium', 'attribute_id': att_size.id, 'sequence': 2})
+
+ # Create Template Product
+ product_template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'attribute_line_ids': [
+ (0, 0, {
+ 'attribute_id': att_color.id,
+ 'value_ids': [(6, 0, [att_color_red.id, att_color_blue.id])]
+ }),
+ (0, 0, {
+ 'attribute_id': att_size.id,
+ 'value_ids': [(6, 0, [att_size_big.id, att_size_medium.id])]
+ })
+ ]
+ })
+
+ sofa_red = product_template.attribute_line_ids[0].product_template_value_ids[0]
+ sofa_blue = product_template.attribute_line_ids[0].product_template_value_ids[1]
+
+ sofa_big = product_template.attribute_line_ids[1].product_template_value_ids[0]
+ sofa_medium = product_template.attribute_line_ids[1].product_template_value_ids[1]
+
+ # Create components Of BOM
+ product_A = Product.create({
+ 'name': 'Wood'})
+ product_B = Product.create({
+ 'name': 'Clothes'})
+
+ # Create BOM
+ self.env['mrp.bom'].create({
+ 'product_tmpl_id': product_template.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {
+ 'product_id': product_A.id,
+ 'product_qty': 1,
+ 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id), (4, sofa_big.id)],
+ }),
+ (0, 0, {
+ 'product_id': product_B.id,
+ 'product_qty': 1,
+ 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id)]
+ })
+ ]
+ })
+
+ dict_consumed_products = {
+ sofa_red + sofa_big: product_A + product_B,
+ sofa_red + sofa_medium: product_B,
+ sofa_blue + sofa_big: product_A + product_B,
+ sofa_blue + sofa_medium: product_B,
+ }
+
+ # Create production order for all variants.
+ for combination, consumed_products in dict_consumed_products.items():
+ product = product_template.product_variant_ids.filtered(lambda p: p.product_template_attribute_value_ids == combination)
+ mrp_order_form = Form(self.env['mrp.production'])
+ mrp_order_form.product_id = product
+ mrp_order = mrp_order_form.save()
+
+ # Check consumed materials in production order.
+ self.assertEqual(mrp_order.move_raw_ids.product_id, consumed_products)
+
+ def test_13_bom_kit_qty(self):
+ self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'phantom',
+ 'bom_line_ids': [
+ (0, 0, {
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ }),
+ (0, 0, {
+ 'product_id': self.product_3.id,
+ 'product_qty': 2,
+ })
+ ]
+ })
+ location = self.env.ref('stock.stock_location_stock')
+ self.env['stock.quant']._update_available_quantity(self.product_2, location, 4.0)
+ self.env['stock.quant']._update_available_quantity(self.product_3, location, 8.0)
+ # Force the kit product available qty to be computed at the same time than its component quantities
+ # Because `qty_available` of a bom kit "recurse" on `qty_available` of its component,
+ # and this is a tricky thing for the ORM:
+ # `qty_available` gets called for `product_7_3`, `product_2` and `product_3`
+ # which then recurse on calling `qty_available` for `product_2` and `product_3` to compute the quantity of
+ # the kit `product_7_3`. `product_2` and `product_3` gets protected at the first call of the compute method,
+ # ending the recurse call to not call the compute method and just left the Falsy value `0.0`
+ # for the components available qty.
+ kit_product_qty, _, _ = (self.product_7_3 + self.product_2 + self.product_3).mapped("qty_available")
+ self.assertEqual(kit_product_qty, 2)
+
+ def test_20_bom_report(self):
+ """ Simulate a crumble receipt with mrp and open the bom structure
+ report and check that data insde are correct.
+ """
+ uom_kg = self.env.ref('uom.product_uom_kgm')
+ uom_litre = self.env.ref('uom.product_uom_litre')
+ crumble = self.env['product.product'].create({
+ 'name': 'Crumble',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ })
+ butter = self.env['product.product'].create({
+ 'name': 'Butter',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ 'standard_price': 7.01
+ })
+ biscuit = self.env['product.product'].create({
+ 'name': 'Biscuit',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ 'standard_price': 1.5
+ })
+ bom_form_crumble = Form(self.env['mrp.bom'])
+ bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
+ bom_form_crumble.product_qty = 11
+ bom_form_crumble.product_uom_id = uom_kg
+ bom_crumble = bom_form_crumble.save()
+
+ workcenter = self.env['mrp.workcenter'].create({
+ 'costs_hour': 10,
+ 'name': 'Deserts Table'
+ })
+
+ with Form(bom_crumble) as bom:
+ with bom.bom_line_ids.new() as line:
+ line.product_id = butter
+ line.product_uom_id = uom_kg
+ line.product_qty = 5
+ with bom.bom_line_ids.new() as line:
+ line.product_id = biscuit
+ line.product_uom_id = uom_kg
+ line.product_qty = 6
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Prepare biscuits'
+ operation.time_cycle_manual = 5
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Prepare butter'
+ operation.time_cycle_manual = 3
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Mix manually'
+ operation.time_cycle_manual = 5
+
+ # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
+ # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
+ self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == butter.id:
+ # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
+ self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0)
+ if component_line['prod_id'] == biscuit.id:
+ # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
+ self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0)
+ # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21
+ self.assertEqual(float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct')
+ self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
+
+ # TEST BOM STRUCTURE VALUE BY UNIT
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
+ # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
+ self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == butter.id:
+ # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
+ self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
+ if component_line['prod_id'] == biscuit.id:
+ # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
+ self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
+ # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
+ self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
+
+ # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
+ report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
+ report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
+ operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round(6 / 60 * 10, precision_digits=2)
+ # Both needs 2 operation cycle
+ self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost'])
+ self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost)
+ report_values_23 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=23, searchVariant=False)
+ operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round(9 / 60 * 10, precision_digits=2)
+ self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost)
+
+ # Create a more complex BoM with a sub product
+ cheese_cake = self.env['product.product'].create({
+ 'name': 'Cheese Cake 300g',
+ 'type': 'product',
+ })
+ cream = self.env['product.product'].create({
+ 'name': 'cream',
+ 'type': 'product',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id,
+ 'standard_price': 5.17,
+ })
+ bom_form_cheese_cake = Form(self.env['mrp.bom'])
+ bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
+ bom_form_cheese_cake.product_qty = 60
+ bom_form_cheese_cake.product_uom_id = self.uom_unit
+ bom_cheese_cake = bom_form_cheese_cake.save()
+
+ workcenter_2 = self.env['mrp.workcenter'].create({
+ 'name': 'cake mounting',
+ 'costs_hour': 20,
+ 'time_start': 10,
+ 'time_stop': 15
+ })
+
+ with Form(bom_cheese_cake) as bom:
+ with bom.bom_line_ids.new() as line:
+ line.product_id = cream
+ line.product_uom_id = uom_litre
+ line.product_qty = 3
+ with bom.bom_line_ids.new() as line:
+ line.product_id = crumble
+ line.product_uom_id = uom_kg
+ line.product_qty = 5.4
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Mix cheese and crumble'
+ operation.time_cycle_manual = 10
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter_2
+ operation.name = 'Cake mounting'
+ operation.time_cycle_manual = 5
+
+
+ # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
+ self.assertEqual(report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0)
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == cream.id:
+ # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
+ self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0)
+ if component_line['prod_id'] == crumble.id:
+ # 5.4 kg of crumble at the cost of a batch.
+ crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total']
+ self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0)
+ # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost
+ self.assertEqual(float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
+
+ def test_21_bom_report_variant(self):
+ """ Test a sub BoM process with multiple variants.
+ BOM 1:
+ product template = car
+ quantity = 5 units
+ - red paint 50l -> red car (product.product)
+ - blue paint 50l -> blue car
+ - red dashboard with gps -> red car with GPS
+ - red dashboard w/h gps -> red w/h GPS
+ - blue dashboard with gps -> blue car with GPS
+ - blue dashboard w/h gps -> blue w/h GPS
+
+ BOM 2:
+ product_tmpl = dashboard
+ quantity = 2
+ - red paint 1l -> red dashboard (product.product)
+ - blue paint 1l -> blue dashboard
+ - gps -> dashboard with gps
+
+ Check the Price for a Blue Car with GPS -> 910$:
+ 10l of blue paint -> 200$
+ 1 blue dashboard GPS -> 710$:
+ - 0.5l of blue paint -> 10$
+ - GPS -> 700$
+
+ Check the price for a red car -> 10.5l of red paint -> 210$
+ """
+ # Create a product template car with attributes gps(yes, no), color(red, blue)
+ self.car = self.env['product.template'].create({
+ 'name': 'Car',
+ })
+ self.gps_attribute = self.env['product.attribute'].create({'name': 'GPS', 'sequence': 1})
+ self.gps_yes = self.env['product.attribute.value'].create({
+ 'name': 'Yes',
+ 'attribute_id': self.gps_attribute.id,
+ 'sequence': 1,
+ })
+ self.gps_no = self.env['product.attribute.value'].create({
+ 'name': 'No',
+ 'attribute_id': self.gps_attribute.id,
+ 'sequence': 2,
+ })
+
+ self.car_gps_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.car.id,
+ 'attribute_id': self.gps_attribute.id,
+ 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
+ })
+ self.car_gps_yes = self.car_gps_attribute_line.product_template_value_ids[0]
+ self.car_gps_no = self.car_gps_attribute_line.product_template_value_ids[1]
+
+ self.color_attribute = self.env['product.attribute'].create({'name': 'Color', 'sequence': 1})
+ self.color_red = self.env['product.attribute.value'].create({
+ 'name': 'Red',
+ 'attribute_id': self.color_attribute.id,
+ 'sequence': 1,
+ })
+ self.color_blue = self.env['product.attribute.value'].create({
+ 'name': 'Blue',
+ 'attribute_id': self.color_attribute.id,
+ 'sequence': 2,
+ })
+
+ self.car_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.car.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.car_color_red = self.car_color_attribute_line.product_template_value_ids[0]
+ self.car_color_blue = self.car_color_attribute_line.product_template_value_ids[1]
+
+ # Blue and red paint
+ uom_litre = self.env.ref('uom.product_uom_litre')
+ self.paint = self.env['product.template'].create({
+ 'name': 'Paint',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id
+ })
+ self.paint_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.paint.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.paint_color_red = self.paint_color_attribute_line.product_template_value_ids[0]
+ self.paint_color_blue = self.paint_color_attribute_line.product_template_value_ids[1]
+
+ self.paint.product_variant_ids.write({'standard_price': 20})
+
+ self.dashboard = self.env['product.template'].create({
+ 'name': 'Dashboard',
+ 'standard_price': 1000,
+ })
+
+ self.dashboard_gps_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.dashboard.id,
+ 'attribute_id': self.gps_attribute.id,
+ 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
+ })
+ self.dashboard_gps_yes = self.dashboard_gps_attribute_line.product_template_value_ids[0]
+ self.dashboard_gps_no = self.dashboard_gps_attribute_line.product_template_value_ids[1]
+
+ self.dashboard_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.dashboard.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.dashboard_color_red = self.dashboard_color_attribute_line.product_template_value_ids[0]
+ self.dashboard_color_blue = self.dashboard_color_attribute_line.product_template_value_ids[1]
+
+ self.gps = self.env['product.product'].create({
+ 'name': 'GPS',
+ 'standard_price': 700,
+ })
+
+ bom_form_car = Form(self.env['mrp.bom'])
+ bom_form_car.product_tmpl_id = self.car
+ bom_form_car.product_qty = 5
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
+ line.product_uom_id = uom_litre
+ line.product_qty = 50
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
+ line.product_uom_id = uom_litre
+ line.product_qty = 50
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_red)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_blue)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_red)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_blue)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ bom_car = bom_form_car.save()
+
+ bom_dashboard = Form(self.env['mrp.bom'])
+ bom_dashboard.product_tmpl_id = self.dashboard
+ bom_dashboard.product_qty = 2
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
+ line.product_uom_id = uom_litre
+ line.product_qty = 1
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_color_red)
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
+ line.product_uom_id = uom_litre
+ line.product_qty = 1
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_color_blue)
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.gps
+ line.product_qty = 2
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_gps_yes)
+ bom_dashboard = bom_dashboard.save()
+
+ blue_car_with_gps = self.car._get_variant_for_combination(self.car_color_blue + self.car_gps_yes)
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=blue_car_with_gps.id)
+ # Two lines. blue dashboard with gps and blue paint.
+ self.assertEqual(len(report_values['lines']['components']), 2)
+
+ # 10l of blue paint
+ blue_paint = self.paint._get_variant_for_combination(self.paint_color_blue)
+ self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['prod_id'])
+ self.assertEqual(report_values['lines']['components'][0]['prod_qty'], 10)
+ # 1 blue dashboard with GPS
+ blue_dashboard_gps = self.dashboard._get_variant_for_combination(self.dashboard_color_blue + self.dashboard_gps_yes)
+ self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['prod_id'])
+ self.assertEqual(report_values['lines']['components'][1]['prod_qty'], 1)
+ component = report_values['lines']['components'][1]
+ report_values_dashboad = self.env['report.mrp.report_bom_structure']._get_bom(
+ component['child_bom'], component['prod_id'], component['prod_qty'],
+ component['line_id'], component['level'] + 1)
+
+ self.assertEqual(len(report_values_dashboad['components']), 2)
+ self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['prod_id'])
+ self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['prod_id'])
+
+ # 0.5l of paint at price of 20$/litre -> 10$
+ self.assertEqual(report_values_dashboad['components'][0]['total'], 10)
+ # GPS 700$
+ self.assertEqual(report_values_dashboad['components'][1]['total'], 700)
+
+ # Dashboard blue with GPS should have a BoM cost of 710$
+ self.assertEqual(report_values['lines']['components'][1]['total'], 710)
+ # 10l of paint at price of 20$/litre -> 200$
+ self.assertEqual(report_values['lines']['components'][0]['total'], 200)
+
+ # Total cost of blue car with GPS: 10 + 700 + 200 = 910
+ self.assertEqual(report_values['lines']['total'], 910)
+
+ red_car_without_gps = self.car._get_variant_for_combination(self.car_color_red + self.car_gps_no)
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
+ # Same math than before but without GPS
+ self.assertEqual(report_values['lines']['total'], 210)
+
+ def test_22_bom_report_recursive_bom(self):
+ """ Test report with recursive BoM and different quantities.
+ BoM 1:
+ product = Finished (units)
+ quantity = 100 units
+ - Semi-Finished 5 kg
+
+ BoM 2:
+ product = Semi-Finished (kg)
+ quantity = 11 kg
+ - Assembly 2 dozens
+
+ BoM 3:
+ product = Assembly (dozens)
+ quantity = 5 dozens
+ - Raw Material 4 litres (product.product 5$/litre)
+
+ Check the Price for 80 units of Finished -> 2.92$:
+ """
+ # Create a products templates
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_kg = self.env.ref('uom.product_uom_kgm')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ uom_litre = self.env.ref('uom.product_uom_litre')
+
+ finished = self.env['product.product'].create({
+ 'name': 'Finished',
+ 'type': 'product',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+
+ semi_finished = self.env['product.product'].create({
+ 'name': 'Semi-Finished',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ })
+
+ assembly = self.env['product.product'].create({
+ 'name': 'Assembly',
+ 'type': 'product',
+ 'uom_id': uom_dozen.id,
+ 'uom_po_id': uom_dozen.id,
+ })
+
+ raw_material = self.env['product.product'].create({
+ 'name': 'Raw Material',
+ 'type': 'product',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id,
+ 'standard_price': 5,
+ })
+
+ #Create bom
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = finished.product_tmpl_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = semi_finished
+ line.product_uom_id = uom_kg
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
+
+ bom_semi_finished = Form(self.env['mrp.bom'])
+ bom_semi_finished.product_tmpl_id = semi_finished.product_tmpl_id
+ bom_semi_finished.product_qty = 11
+ with bom_semi_finished.bom_line_ids.new() as line:
+ line.product_id = assembly
+ line.product_uom_id = uom_dozen
+ line.product_qty = 2
+ bom_semi_finished = bom_semi_finished.save()
+
+ bom_assembly = Form(self.env['mrp.bom'])
+ bom_assembly.product_tmpl_id = assembly.product_tmpl_id
+ bom_assembly.product_qty = 5
+ with bom_assembly.bom_line_ids.new() as line:
+ line.product_id = raw_material
+ line.product_uom_id = uom_litre
+ line.product_qty = 4
+ bom_assembly = bom_assembly.save()
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
+
+ self.assertAlmostEqual(report_values['lines']['total'], 2.92)
+
+ def test_validate_no_bom_line_with_same_product(self):
+ """
+ Cannot set a BOM line on a BOM with the same product as the BOM itself
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ finished = self.env['product.product'].create({
+ 'name': 'Finished',
+ 'type': 'product',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = finished.product_tmpl_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = finished
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ bom_finished = bom_finished.save()
+
+ def test_validate_no_bom_line_with_same_product_variant(self):
+ """
+ Cannot set a BOM line on a BOM with the same product variant as the BOM itself
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_7_template
+ bom_finished.product_id = self.product_7_3
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_3
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ bom_finished = bom_finished.save()
+
+ def test_validate_bom_line_with_different_product_variant(self):
+ """
+ Can set a BOM line on a BOM with a different product variant as the BOM itself (same product)
+ Usecase for example A black T-shirt made from a white T-shirt and
+ black color.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_7_template
+ bom_finished.product_id = self.product_7_3
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_2
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
+
+ def test_validate_bom_line_with_variant_of_bom_product(self):
+ """
+ Can set a BOM line on a BOM with a product variant when the BOM has no variant selected
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_6.product_tmpl_id
+ # no product_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_2
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ bom_finished = bom_finished.save()