# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import Form, TransactionCase, tagged @tagged('post_install', '-at_install') class TestSaleMrpKitBom(TransactionCase): def _create_product(self, name, storable, price): return self.env['product.product'].create({ 'name': name, 'is_storable': storable, 'standard_price': price, }) def test_reset_avco_kit(self): """ Test a specific use case : One product with 2 variant, each variant has its own BoM with either component_1 or component_2. Create a SO for one of the variant, confirm, cancel, reset to draft and then change the product of the SO -> There should be no traceback """ component_1 = self.env['product.product'].create({'name': 'compo 1'}) component_2 = self.env['product.product'].create({'name': 'compo 2'}) product_category = self.env['product.category'].create({ 'name': 'test avco kit', 'property_cost_method': 'average' }) attributes = self.env['product.attribute'].create({'name': 'Legs'}) steel_legs = self.env['product.attribute.value'].create({'attribute_id': attributes.id, 'name': 'Steel'}) aluminium_legs = self.env['product.attribute.value'].create( {'attribute_id': attributes.id, 'name': 'Aluminium'}) product_template = self.env['product.template'].create({ 'name': 'test product', 'categ_id': product_category.id, 'attribute_line_ids': [(0, 0, { 'attribute_id': attributes.id, 'value_ids': [(6, 0, [steel_legs.id, aluminium_legs.id])] })] }) product_variant_ids = product_template.product_variant_ids # BoM 1 with component_1 self.env['mrp.bom'].create({ 'product_id': product_variant_ids[0].id, 'product_tmpl_id': product_variant_ids[0].product_tmpl_id.id, 'product_qty': 1.0, 'consumption': 'flexible', 'type': 'phantom', 'bom_line_ids': [(0, 0, {'product_id': component_1.id, 'product_qty': 1})] }) # BoM 2 with component_2 self.env['mrp.bom'].create({ 'product_id': product_variant_ids[1].id, 'product_tmpl_id': product_variant_ids[1].product_tmpl_id.id, 'product_qty': 1.0, 'consumption': 'flexible', 'type': 'phantom', 'bom_line_ids': [(0, 0, {'product_id': component_2.id, 'product_qty': 1})] }) partner = self.env['res.partner'].create({'name': 'Testing Man'}) so = self.env['sale.order'].create({ 'partner_id': partner.id, }) # Create the order line self.env['sale.order.line'].create({ 'name': "Order line", 'product_id': product_variant_ids[0].id, 'order_id': so.id, }) so.action_confirm() so._action_cancel() so.action_draft() with Form(so) as so_form: with so_form.order_line.edit(0) as order_line_change: # The actual test, there should be no traceback here order_line_change.product_id = product_variant_ids[1] def test_sale_mrp_kit_cost(self): """ Check the total cost of a KIT: # BoM of Kit A: # - BoM Type: Kit # - Quantity: 1 # - Components: # * 1 x Component A (Cost: $ 6, QTY: 1, UOM: Dozens) # * 1 x Component B (Cost: $ 10, QTY: 2, UOM: Unit) # cost of Kit A = (6 * 1 * 12) + (10 * 2) = $ 92 """ self.customer = self.env['res.partner'].create({ 'name': 'customer' }) self.kit_product = self._create_product('Kit Product', True, 1.00) # Creating components self.component_a = self._create_product('Component A', True, 1.00) self.component_a.product_tmpl_id.standard_price = 6 self.component_b = self._create_product('Component B', True, 1.00) self.component_b.product_tmpl_id.standard_price = 10 cat = self.env['product.category'].create({ 'name': 'fifo', 'property_cost_method': 'fifo' }) self.kit_product.product_tmpl_id.categ_id = cat self.component_a.product_tmpl_id.categ_id = cat self.component_b.product_tmpl_id.categ_id = cat self.bom = self.env['mrp.bom'].create({ 'product_tmpl_id': self.kit_product.product_tmpl_id.id, 'product_qty': 1.0, 'type': 'phantom' }) self.env['mrp.bom.line'].create({ 'product_id': self.component_a.id, 'product_qty': 1.0, 'bom_id': self.bom.id, 'product_uom_id': self.env.ref('uom.product_uom_dozen').id, }) self.env['mrp.bom.line'].create({ 'product_id': self.component_b.id, 'product_qty': 2.0, 'bom_id': self.bom.id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, }) # Create a SO with one unit of the kit product so = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [ (0, 0, { 'name': self.kit_product.name, 'product_id': self.kit_product.id, 'product_uom_qty': 1.0, 'product_uom': self.kit_product.uom_id.id, })], }) so.action_confirm() line = so.order_line purchase_price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids) self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure") def test_qty_delivered_with_bom(self): """Check the quantity delivered, when a bom line has a non integer quantity""" self.env.ref('product.decimal_product_uom').digits = 5 self.kit = self._create_product('Kit', True, 0.00) self.comp = self._create_product('Component', True, 0.00) # Create BoM for Kit bom_product_form = Form(self.env['mrp.bom']) bom_product_form.product_id = self.kit bom_product_form.product_tmpl_id = self.kit.product_tmpl_id bom_product_form.product_qty = 1.0 bom_product_form.type = 'phantom' with bom_product_form.bom_line_ids.new() as bom_line: bom_line.product_id = self.comp bom_line.product_qty = 0.08600 self.bom = bom_product_form.save() self.customer = self.env['res.partner'].create({ 'name': 'customer', }) so = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [ (0, 0, { 'name': self.kit.name, 'product_id': self.kit.id, 'product_uom_qty': 10.0, 'product_uom': self.kit.uom_id.id, 'price_unit': 1, 'tax_id': False, })], }) so.action_confirm() self.assertTrue(so.picking_ids) self.assertEqual(so.order_line.qty_delivered, 0) picking = so.picking_ids picking.move_ids.write({'quantity': 0.86000, 'picked': True}) picking.button_validate() # Checks the delivery amount (must be 10). self.assertEqual(so.order_line.qty_delivered, 10) def test_qty_delivered_with_bom_using_kit(self): """Check the quantity delivered, when one product is a kit and his bom uses another product that is also a kit""" self.kitA = self._create_product('Kit A', False, 0.00) self.kitB = self._create_product('Kit B', False, 0.00) self.compA = self._create_product('ComponentA', False, 0.00) self.compB = self._create_product('ComponentB', False, 0.00) # Create BoM for KitB bom_product_formA = Form(self.env['mrp.bom']) bom_product_formA.product_id = self.kitB bom_product_formA.product_tmpl_id = self.kitB.product_tmpl_id bom_product_formA.product_qty = 1.0 bom_product_formA.type = 'phantom' with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = self.compA bom_line.product_qty = 1 with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = self.compB bom_line.product_qty = 1 self.bomA = bom_product_formA.save() # Create BoM for KitA bom_product_formB = Form(self.env['mrp.bom']) bom_product_formB.product_id = self.kitA bom_product_formB.product_tmpl_id = self.kitA.product_tmpl_id bom_product_formB.product_qty = 1.0 bom_product_formB.type = 'phantom' with bom_product_formB.bom_line_ids.new() as bom_line: bom_line.product_id = self.compA bom_line.product_qty = 1 with bom_product_formB.bom_line_ids.new() as bom_line: bom_line.product_id = self.kitB bom_line.product_qty = 1 self.bomB = bom_product_formB.save() self.customer = self.env['res.partner'].create({ 'name': 'customer', }) so = self.env['sale.order'].create({ 'partner_id': self.customer.id, 'order_line': [ (0, 0, { 'name': self.kitA.name, 'product_id': self.kitA.id, 'product_uom_qty': 1.0, 'product_uom': self.kitA.uom_id.id, 'price_unit': 1, 'tax_id': False, })], }) so.action_confirm() self.assertTrue(so.picking_ids) self.assertEqual(so.order_line.qty_delivered, 0) picking = so.picking_ids picking.button_validate() # Checks the delivery amount (must be 1). self.assertEqual(so.order_line.qty_delivered, 1) def test_sale_kit_show_kit_in_delivery(self): """Create a kit with 2 product and activate 2 steps delivery and check that every stock move contains a bom_line_id """ wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1) wh.write({'delivery_steps': 'pick_ship'}) kitA = self._create_product('Kit Product', True, 0.00) compA = self._create_product('ComponentA', True, 0.00) compB = self._create_product('ComponentB', True, 0.00) # Create BoM for KitB bom_product_formA = Form(self.env['mrp.bom']) bom_product_formA.product_id = kitA bom_product_formA.product_tmpl_id = kitA.product_tmpl_id bom_product_formA.product_qty = 1.0 bom_product_formA.type = 'phantom' with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = compA bom_line.product_qty = 1 with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = compB bom_line.product_qty = 1 bom_product_formA.save() customer = self.env['res.partner'].create({ 'name': 'customer', }) so = self.env['sale.order'].create({ 'partner_id': customer.id, 'order_line': [ (0, 0, { 'name': kitA.name, 'product_id': kitA.id, 'product_uom_qty': 1.0, 'product_uom': kitA.uom_id.id, 'price_unit': 1, 'tax_id': False, })] }) so.action_confirm() pick = so.picking_ids[0] self.assertTrue(pick.move_ids_without_package[0].bom_line_id, "All component from kits should have a bom line") self.assertTrue(pick.move_ids_without_package[1].bom_line_id, "All component from kits should have a bom line") pick.move_ids.write({'quantity': 1, 'picked': True}) pick.button_validate() ship = so.picking_ids[1] self.assertTrue(ship.move_ids_without_package[0].bom_line_id, "All component from kits should have a bom line") self.assertTrue(ship.move_ids_without_package[1].bom_line_id, "All component from kits should have a bom line") def test_qty_delivered_with_bom_using_kit2(self): """Create 2 kits products that have common components and activate 2 steps delivery Then create a sale order with these 2 products, and put everything in a pack in the first step of the delivery. After the shipping is done, check the done quantity is correct for each products. """ wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1) wh.write({'delivery_steps': 'pick_ship'}) kitAB = self._create_product('Kit AB', True, 0.00) kitABC = self._create_product('Kit ABC', True, 0.00) compA = self._create_product('ComponentA', True, 0.00) compB = self._create_product('ComponentB', True, 0.00) compC = self._create_product('ComponentC', True, 0.00) # Create BoM for KitB bom_product_formA = Form(self.env['mrp.bom']) bom_product_formA.product_id = kitAB bom_product_formA.product_tmpl_id = kitAB.product_tmpl_id bom_product_formA.product_qty = 1.0 bom_product_formA.type = 'phantom' with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = compA bom_line.product_qty = 1 with bom_product_formA.bom_line_ids.new() as bom_line: bom_line.product_id = compB bom_line.product_qty = 1 bom_product_formA.save() # Create BoM for KitA bom_product_formB = Form(self.env['mrp.bom']) bom_product_formB.product_id = kitABC bom_product_formB.product_tmpl_id = kitABC.product_tmpl_id bom_product_formB.product_qty = 1.0 bom_product_formB.type = 'phantom' with bom_product_formB.bom_line_ids.new() as bom_line: bom_line.product_id = compA bom_line.product_qty = 1 with bom_product_formB.bom_line_ids.new() as bom_line: bom_line.product_id = compB bom_line.product_qty = 1 with bom_product_formB.bom_line_ids.new() as bom_line: bom_line.product_id = compC bom_line.product_qty = 1 bom_product_formB.save() customer = self.env['res.partner'].create({ 'name': 'customer', }) so = self.env['sale.order'].create({ 'partner_id': customer.id, 'order_line': [ (0, 0, { 'name': kitAB.name, 'product_id': kitAB.id, 'product_uom_qty': 1.0, 'product_uom': kitAB.uom_id.id, 'price_unit': 1, 'tax_id': False, }), (0, 0, { 'name': kitABC.name, 'product_id': kitABC.id, 'product_uom_qty': 1.0, 'product_uom': kitABC.uom_id.id, 'price_unit': 1, 'tax_id': False, })], }) so.action_confirm() pick = so.picking_ids[0] for move in pick.move_ids: move.write({'quantity': 1, 'picked': True}) pick.action_put_in_pack() pick.button_validate() ship = so.picking_ids[1] ship.package_level_ids.write({'is_done': True}) ship.package_level_ids._set_is_done() for move_line in ship.move_line_ids: self.assertEqual(move_line.move_id.product_uom_qty, move_line.quantity, "Quantity done should be equal to the quantity reserved in the move line") def test_kit_in_delivery_slip(self): """ Suppose this structure: Sale order: - Kit 1 with a sales description("test"): |- Compo 1 - Product 1 - Kit 2 * Variant 1 - Compo 1 * Variant 2 - Compo 1 - Kit 4: - Compo 1 - Kit 5 - Kit 4 - Compo 1 This test ensures that, when delivering a Kit product with a sales description, the delivery report is correctly printed with all the products. """ kit_1, component_1, product_1, kit_3, kit_4 = self.env['product.product'].create([{ 'name': n, 'is_storable': True, } for n in ['Kit 1', 'Compo 1', 'Product 1', 'Kit 3', 'Kit 4']]) kit_1.description_sale = "test" self.env['mrp.bom'].create([{ 'product_tmpl_id': kit_1.product_tmpl_id.id, 'product_qty': 1, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': component_1.id, 'product_qty': 1}), ], }]) colors = ['red', 'blue'] prod_attr = self.env['product.attribute'].create({'name': 'Color', 'create_variant': 'always'}) prod_attr_values = self.env['product.attribute.value'].create([{'name': color, 'attribute_id': prod_attr.id, 'sequence': 1} for color in colors]) kit_2 = self.env['product.template'].create({ 'name': 'Kit 2', 'attribute_line_ids': [(0, 0, { 'attribute_id': prod_attr.id, 'value_ids': [(6, 0, prod_attr_values.ids)] })] }) self.env['mrp.bom'].create([{ 'product_tmpl_id': kit_2.id, 'product_id': kit_2.product_variant_ids[0].id, 'product_qty': 1, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': component_1.id, 'product_qty': 1}), ], }]) self.env['mrp.bom'].create([{ 'product_tmpl_id': kit_2.id, 'product_id': kit_2.product_variant_ids[1].id, 'product_qty': 1, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': component_1.id, 'product_qty': 1}), ], }]) self.env['mrp.bom'].create([{ 'product_tmpl_id': kit_3.product_tmpl_id.id, 'product_qty': 1, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': component_1.id, 'product_qty': 1}), ], }]) self.env['mrp.bom'].create([{ 'product_tmpl_id': kit_4.product_tmpl_id.id, 'product_qty': 1, 'type': 'phantom', 'bom_line_ids': [ (0, 0, {'product_id': component_1.id, 'product_qty': 1}), (0, 0, {'product_id': kit_3.id, 'product_qty': 1}), ], }]) customer = self.env['res.partner'].create({ 'name': 'customer', }) so = self.env['sale.order'].create({ 'partner_id': customer.id, 'order_line': [ (0, 0, { 'product_id': kit_1.id, 'product_uom_qty': 1.0, }), (0, 0, { 'product_id': product_1.id, 'product_uom_qty': 1.0, }), (0, 0, { 'product_id': kit_2.product_variant_ids[0].id, 'product_uom_qty': 1.0, }), (0, 0, { 'product_id': kit_2.product_variant_ids[1].id, 'product_uom_qty': 1.0, }), (0, 0, { 'product_id': kit_3.id, 'product_uom_qty': 1.0, }), (0, 0, { 'product_id': kit_4.id, 'product_uom_qty': 1.0, })], }) so.action_confirm() picking = so.picking_ids self.assertEqual(len(so.picking_ids.move_ids_without_package), 7) picking.move_ids.write({'quantity': 1, 'picked': True}) picking.button_validate() self.assertEqual(picking.state, 'done') html_report = self.env['ir.actions.report']._render_qweb_html('stock.report_deliveryslip', picking.ids)[0].decode('utf-8').split('\n') keys = [ "Kit 1", "Compo 1", "Kit 2 (red)", "Compo 1", "Kit 2 (blue)", "Compo 1", "Kit 3", "Compo 1", "Kit 4", "Compo 1", "Products not associated with a kit", "Product 1", ] for line in html_report: if not keys: break if keys[0] in line: keys = keys[1:] self.assertFalse(keys, "All keys should be in the report with the defined order") def test_sale_multistep_kit_qty_change(self): warehouse = self.env['stock.warehouse'].search([], limit=1) warehouse.write({'delivery_steps': 'pick_ship'}) self.partner = self.env['res.partner'].create({'name': 'Test Partner'}) kit_prod = self._create_product('kit_prod', 'product', 0.00) sub_kit = self._create_product('sub_kit', 'product', 0.00) component = self._create_product('component', 'product', 0.00) component.uom_id = self.env.ref('uom.product_uom_dozen') self.env['stock.quant']._update_available_quantity(component, warehouse.lot_stock_id, 30) # 6 kit_prod == 5 component self.env['mrp.bom'].create([{ # 2 kit_prod == 5 sub_kit 'product_tmpl_id': kit_prod.product_tmpl_id.id, 'product_qty': 2.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, { 'product_id': sub_kit.id, 'product_qty': 5, })], }, { # 3 sub_kit == 1 component 'product_tmpl_id': sub_kit.product_tmpl_id.id, 'product_qty': 3.0, 'type': 'phantom', 'bom_line_ids': [(0, 0, { 'product_id': component.id, 'product_qty': 1, })], }]) so = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'order_line': [(0, 0, { 'name': kit_prod.name, 'product_id': kit_prod.id, 'product_uom_qty': 30, })], }) # Validate the SO so.action_confirm() picking_pick = so.picking_ids[0] picking_pick.picking_type_id.create_backorder = 'never' # Check the component qty in the created picking should be 25 self.assertEqual(picking_pick.move_ids.product_qty, 30 * 5 / 6) # Update the kit quantity in the SO so.order_line[0].product_uom_qty = 60 # Check the component qty after the update should be 50 self.assertEqual(picking_pick.move_ids.product_qty, 60 * 5 / 6) # Deliver half the quantity 25 component == 30 kit_prod picking_pick.move_ids.quantity = 25 picking_pick.button_validate() picking_ship = so.picking_ids[1] picking_ship.picking_type_id.create_backorder = 'never' picking_ship.move_ids.quantity = 25 picking_ship.button_validate() self.assertEqual(so.order_line.qty_delivered, 25 / 5 * 6) # Return 10 components stock_return_picking_form = Form(self.env['stock.return.picking'] .with_context(active_ids=picking_ship.ids, active_id=picking_ship.id, active_model='stock.picking')) return_wiz = stock_return_picking_form.save() for return_move in return_wiz.product_return_moves: return_move.write({ 'quantity': 10, 'to_refund': True }) res = return_wiz.action_create_returns() return_pick = self.env['stock.picking'].browse(res['res_id']) # Process all components and validate the return return_pick.button_validate() self.assertEqual(so.order_line.qty_delivered, 15 / 5 * 6) # Resend 5 components stock_return_picking_form = Form(self.env['stock.return.picking'] .with_context(active_ids=return_pick.ids, active_id=return_pick.id, active_model='stock.picking')) return_wiz = stock_return_picking_form.save() for return_move in return_wiz.product_return_moves: return_move.write({ 'quantity': 5, 'to_refund': True }) res = return_wiz.action_create_returns() # Validate the return self.env['stock.picking'].browse(res['res_id']).button_validate() self.assertEqual(so.order_line.qty_delivered, 20 / 5 * 6) def test_sale_kit_qty_change(self): # Create record rule mrp_bom_model = self.env['ir.model']._get('mrp.bom') self.env['ir.rule'].create({ 'name': "No one allowed to access BoMs", 'model_id': mrp_bom_model.id, 'domain_force': [(0, '=', 1)], }) # Create BoM kit_product = self._create_product('Kit Product', 'product', 1) component_a = self._create_product('Component A', 'product', 1) self.env['mrp.bom'].create({ 'product_id': kit_product.id, 'product_tmpl_id': kit_product.product_tmpl_id.id, 'product_qty': 1, 'consumption': 'flexible', 'type': 'phantom', 'bom_line_ids': [(0, 0, {'product_id': component_a.id, 'product_qty': 1})] }) # Create sale order partner = self.env['res.partner'].create({'name': 'Testing Man'}) so = self.env['sale.order'].create({ 'partner_id': partner.id, }) sol = self.env['sale.order.line'].create({ 'name': "Order line", 'product_id': kit_product.id, 'order_id': so.id, }) so.action_confirm() user_admin = self.env['res.users'].search([('login', '=', 'admin')]) sol.with_user(user_admin).write({'product_uom_qty': 5}) self.assertEqual(sum(sol.move_ids.mapped('product_uom_qty')), 5)