# Part of Odoo. See LICENSE file for full copyright and licensing details.

from unittest import skip

from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import Form, common
from odoo.tools import float_compare, mute_logger

from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import (
    ValuationReconciliationTestCommon,
)


# these tests create accounting entries, and therefore need a chart of accounts
class TestSaleMrpFlowCommon(ValuationReconciliationTestCommon, TestSaleCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # Required for `uom_id` to be visible in the view
        cls._enable_uom()
        cls.env.ref('stock.route_warehouse0_mto').active = True

        # Useful models
        cls.StockMove = cls.env['stock.move']
        cls.UoM = cls.env['uom.uom']
        cls.MrpProduction = cls.env['mrp.production']
        cls.Quant = cls.env['stock.quant']
        cls.ProductCategory = cls.env['product.category']

        cls.uom_kg = cls.uom_kgm
        cls.uom_gm = cls.uom_gram
        cls.uom_ten = cls.UoM.create({
            'name': 'Test-Ten',
            'relative_factor': 10,
            'relative_uom_id': cls.uom_unit.id,
        })

        # Creating all components
        cls.component_a = cls._cls_create_product('Comp A', cls.uom_unit)
        cls.component_b = cls._cls_create_product('Comp B', cls.uom_unit)
        cls.component_c = cls._cls_create_product('Comp C', cls.uom_unit)
        cls.component_d = cls._cls_create_product('Comp D', cls.uom_unit)
        cls.component_e = cls._cls_create_product('Comp E', cls.uom_unit)
        cls.component_f = cls._cls_create_product('Comp F', cls.uom_unit)
        cls.component_g = cls._cls_create_product('Comp G', cls.uom_unit)

        # Create a kit 'kit_1' :
        # -----------------------
        #
        # kit_1 --|- component_a   x2
        #         |- component_b   x1
        #         |- component_c   x3

        cls.kit_1 = cls._cls_create_product('Kit 1', cls.uom_unit)

        cls.bom_kit_1 = cls.env['mrp.bom'].create({
            'product_tmpl_id': cls.kit_1.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine = cls.env['mrp.bom.line']
        BomLine.create({
            'product_id': cls.component_a.id,
            'product_qty': 2.0,
            'bom_id': cls.bom_kit_1.id})
        BomLine.create({
            'product_id': cls.component_b.id,
            'product_qty': 1.0,
            'bom_id': cls.bom_kit_1.id})
        BomLine.create({
            'product_id': cls.component_c.id,
            'product_qty': 3.0,
            'bom_id': cls.bom_kit_1.id})

        # Create a kit 'kit_parent' :
        # ---------------------------
        #
        # kit_parent --|- kit_2 x2 --|- component_d x1
        #              |             |- kit_1 x2 -------|- component_a   x2
        #              |                                |- component_b   x1
        #              |                                |- component_c   x3
        #              |
        #              |- kit_3 x1 --|- component_f x1
        #              |             |- component_g x2
        #              |
        #              |- component_e x1

        # Creating all kits
        cls.kit_2 = cls._cls_create_product('Kit 2', cls.uom_unit)
        cls.kit_3 = cls._cls_create_product('kit 3', cls.uom_unit)
        cls.kit_parent = cls._cls_create_product('Kit Parent', cls.uom_unit)

        # Linking the kits and the components via some 'phantom' BoMs
        bom_kit_2 = cls.env['mrp.bom'].create({
            'product_tmpl_id': cls.kit_2.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine.create({
            'product_id': cls.component_d.id,
            'product_qty': 1.0,
            'bom_id': bom_kit_2.id})
        BomLine.create({
            'product_id': cls.kit_1.id,
            'product_qty': 2.0,
            'bom_id': bom_kit_2.id})

        bom_kit_parent = cls.env['mrp.bom'].create({
            'product_tmpl_id': cls.kit_parent.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine.create({
            'product_id': cls.component_e.id,
            'product_qty': 1.0,
            'bom_id': bom_kit_parent.id})
        BomLine.create({
            'product_id': cls.kit_2.id,
            'product_qty': 2.0,
            'bom_id': bom_kit_parent.id})

        bom_kit_3 = cls.env['mrp.bom'].create({
            'product_tmpl_id': cls.kit_3.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine.create({
            'product_id': cls.component_f.id,
            'product_qty': 1.0,
            'bom_id': bom_kit_3.id})
        BomLine.create({
            'product_id': cls.component_g.id,
            'product_qty': 2.0,
            'bom_id': bom_kit_3.id})

        BomLine.create({
            'product_id': cls.kit_3.id,
            'product_qty': 2.0,
            'bom_id': bom_kit_parent.id})

    @classmethod
    def _cls_create_product(cls, name, uom_id, routes=()):
        p = Form(cls.env['product.product'])
        p.name = name
        p.is_storable = True
        p.uom_id = uom_id
        p.route_ids.clear()
        for r in routes:
            p.route_ids.add(r)
        return p.save()

        # Helper to process quantities based on a dict following this structure :
        #
        # qty_to_process = {
        #     product_id: qty
        # }

    def _process_quantities(self, moves, quantities_to_process):
        """ Helper to process quantities based on a dict following this structure :
            qty_to_process = {
                product_id: qty
            }
        """
        moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
        for move in moves_to_process:
            move.write({
                'quantity': quantities_to_process[move.product_id],
                'picked': True
            })

    def _assert_quantities(self, moves, quantities_to_process):
        """ Helper to check expected quantities based on a dict following this structure :
            qty_to_process = {
                product_id: qty
                ...
            }
        """
        moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
        for move in moves_to_process:
            self.assertEqual(move.product_uom_qty, quantities_to_process[move.product_id])

    def _create_move_quantities(self, qty_to_process, components, warehouse):
        """ Helper to creates moves in order to update the quantities of components
        on a specific warehouse. This ensure that all compute fields are triggered.
        The structure of qty_to_process should be the following :

         qty_to_process = {
            component: (qty, uom),
            ...
        }
        """
        for comp in components:
            f = Form(self.env['stock.move'])
            # <field name="name" invisible="1"/>
            f.location_id = self.env.ref('stock.stock_location_suppliers')
            f.location_dest_id = warehouse.lot_stock_id
            f.product_id = comp
            f.product_uom = qty_to_process[comp][1]
            f.product_uom_qty = qty_to_process[comp][0]
            move = f.save()
            move._action_confirm()
            move._action_assign()
            move_line = move.move_line_ids[0]
            move_line.quantity = qty_to_process[comp][0]
            move._action_done()


@common.tagged('post_install', '-at_install')
class TestSaleMrpFlow(TestSaleMrpFlowCommon):
    @skip('Temporary to fast merge new valuation')
    def test_00_sale_mrp_flow(self):
        """ Test sale to mrp flow with diffrent unit of measure."""


        # Create product A, B, C, D.
        # --------------------------
        route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id
        route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id
        product_a = self._cls_create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto])
        product_c = self._cls_create_product('Product C', self.uom_kg)
        product_b = self._cls_create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto])
        product_d = self._cls_create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto])

        # ------------------------------------------------------------------------------------------
        # Bill of materials for product A, B, D.
        # ------------------------------------------------------------------------------------------

        # Bill of materials for Product A.
        with Form(self.env['mrp.bom']) as f:
            f.product_tmpl_id = product_a.product_tmpl_id
            f.product_qty = 2
            f.product_uom_id = self.uom_dozen
            with f.bom_line_ids.new() as line:
                line.product_id = product_b
                line.product_qty = 3
                line.product_uom_id = self.uom_unit
            with f.bom_line_ids.new() as line:
                line.product_id = product_c
                line.product_qty = 300.0
                line.product_uom_id = self.uom_gm
            with f.bom_line_ids.new() as line:
                line.product_id = product_d
                line.product_qty = 4
                line.product_uom_id = self.uom_unit

        # Bill of materials for Product B.
        with Form(self.env['mrp.bom']) as f:
            f.product_tmpl_id = product_b.product_tmpl_id
            f.product_qty = 1
            f.product_uom_id = self.uom_unit
            f.type = 'phantom'
            with f.bom_line_ids.new() as line:
                line.product_id = product_c
                line.product_qty = 0.400
                line.product_uom_id = self.uom_kg

        # Bill of materials for Product D.
        with Form(self.env['mrp.bom']) as f:
            f.product_tmpl_id = product_d.product_tmpl_id
            f.product_qty = 1
            f.product_uom_id = self.uom_unit
            with f.bom_line_ids.new() as line:
                line.product_id = product_c
                line.product_qty = 1
                line.product_uom_id = self.uom_kg

        # ----------------------------------------
        # Create sales order of 10 Dozen product A.
        # ----------------------------------------

        order_form = Form(self.env['sale.order'])
        order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        with order_form.order_line.new() as line:
            line.product_id = product_a
            line.product_uom_id = self.uom_dozen
            line.product_uom_qty = 10
        order = order_form.save()
        order.action_confirm()

        # Verify buttons are working as expected
        self.assertEqual(order.mrp_production_count, 2, "Mo for product A + child mo for product B")

        # ===============================================================================
        #  Sales order of 10 Dozen product A should create production order
        #  like ..
        # ===============================================================================
        #    Product A  10 Dozen.
        #        Product C  6 kg
        #                As product B phantom in bom A, product A will consume product C
        #                ================================================================
        #                For 1 unit product B it will consume 400 gm
        #                then for 15 unit (Product B 3 unit per 2 Dozen product A)
        #                product B it will consume [ 6 kg ] product C)
        #                Product A will consume 6 kg product C.
        #
        #                [15 * 400 gm ( 6 kg product C)] = 6 kg product C
        #
        #        Product C  1500.0 gm.
        #                [
        #                  For 2 Dozen product A will consume 300.0 gm product C
        #                  then for 10 Dozen product A will consume 1500.0 gm product C.
        #                ]
        #
        #        product D  20 Unit.
        #                [
        #                  For 2 dozen product A will consume 4 unit product D
        #                  then for 10 Dozen product A will consume 20 unit of product D.
        #                ]
        # --------------------------------------------------------------------------------

        # <><><><><><><><><><><><><><><><><><><><>
        # Check manufacturing order for product A.
        # <><><><><><><><><><><><><><><><><><><><>

        # Check quantity, unit of measure and state of manufacturing order.
        # -----------------------------------------------------------------
        self.env['stock.rule'].run_scheduler()
        mnf_product_a = self.env['mrp.production'].search([('product_id', '=', product_a.id)])

        self.assertTrue(mnf_product_a, 'Manufacturing order not created.')
        self.assertEqual(mnf_product_a.product_qty, 10, 'Wrong product quantity in manufacturing order.')
        self.assertEqual(mnf_product_a.product_uom_id, self.uom_dozen, 'Wrong unit of measure in manufacturing order.')
        self.assertEqual(mnf_product_a.state, 'confirmed', 'Manufacturing order should be confirmed.')

        # ------------------------------------------------------------------------------------------
        # Check 'To consume line' for production order of product A.
        # ------------------------------------------------------------------------------------------

        # Check 'To consume line' with product c and uom kg.
        # -------------------------------------------------

        moves = self.StockMove.search([
            ('raw_material_production_id', '=', mnf_product_a.id),
            ('product_id', '=', product_c.id),
            ('product_uom', '=', self.uom_kg.id)])

        # Check total consume line with product c and uom kg.
        self.assertEqual(len(moves), 1, 'Production move lines are not generated proper.')
        list_qty = {move.product_uom_qty for move in moves}
        self.assertEqual(list_qty, {6.0}, "Wrong product quantity in 'To consume line' of manufacturing order.")
        # Check state of consume line with product c and uom kg.
        for move in moves:
            self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.")

        # Check 'To consume line' with product c and uom gm.
        # ---------------------------------------------------

        move = self.StockMove.search([
            ('raw_material_production_id', '=', mnf_product_a.id),
            ('product_id', '=', product_c.id),
            ('product_uom', '=', self.uom_gm.id)])

        # Check total consume line of product c with gm.
        self.assertEqual(len(move), 1, 'Production move lines are not generated proper.')
        # Check quantity should be with 1500.0 ( 2 Dozen product A consume 300.0 gm then 10 Dozen (300.0 * (10/2)).
        self.assertEqual(move.product_uom_qty, 1500.0, "Wrong product quantity in 'To consume line' of manufacturing order.")
        # Check state of consume line with product c with and uom gm.
        self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.")

        # Check 'To consume line' with product D.
        # ---------------------------------------

        move = self.StockMove.search([
            ('raw_material_production_id', '=', mnf_product_a.id),
            ('product_id', '=', product_d.id)])

        # Check total consume line with product D.
        self.assertEqual(len(move), 1, 'Production lines are not generated proper.')

        # <><><><><><><><><><><><><><><><><><><><><><>
        # Manufacturing order for product D (20 unit).
        # <><><><><><><><><><><><><><><><><><><><><><>

        # FP Todo: find a better way to look for the production order
        mnf_product_d = self.MrpProduction.search([('product_id', '=', product_d.id)], order='id desc', limit=1)
        # Check state of production order D.
        self.assertEqual(mnf_product_d.state, 'confirmed', 'Manufacturing order should be confirmed.')

        # Check 'To consume line' state, quantity, uom of production order (product D).
        # -----------------------------------------------------------------------------

        move = self.StockMove.search([('raw_material_production_id', '=', mnf_product_d.id), ('product_id', '=', product_c.id)])
        self.assertEqual(move.product_uom_qty, 20, "Wrong product quantity in 'To consume line' of manufacturing order.")
        self.assertEqual(move.product_uom.id, self.uom_kg.id, "Wrong unit of measure in 'To consume line' of manufacturing order.")
        self.assertEqual(move.state, 'confirmed', "Wrong state in 'To consume line' of manufacturing order.")

        # -------------------------------
        # Create inventory for product c.
        # -------------------------------
        # Need 20 kg product c to produce 20 unit product D.
        # --------------------------------------------------

        self.Quant.with_context(inventory_mode=True).create({
            'product_id': product_c.id, # uom = uom_kg
            'inventory_quantity': 20,
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
        }).action_apply_inventory()

        # --------------------------------------------------
        # Assign product c to manufacturing order of product D.
        # --------------------------------------------------

        mnf_product_d.action_assign()
        self.assertEqual(mnf_product_d.reservation_state, 'assigned', 'Availability should be assigned')
        self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.")

        # ------------------
        # produce product D.
        # ------------------

        mo_form = Form(mnf_product_d)
        mo_form.qty_producing = 20
        mnf_product_d = mo_form.save()
        mnf_product_d.button_mark_done()

        # Check state of manufacturing order.
        self.assertEqual(mnf_product_d.state, 'done', 'Manufacturing order should still be in progress state.')
        # Check available quantity of product D.
        self.assertEqual(product_d.qty_available, 20, 'Wrong quantity available of product D.')

        # -----------------------------------------------------------------
        # Check product D assigned or not to production order of product A.
        # -----------------------------------------------------------------

        self.assertEqual(mnf_product_a.state, 'confirmed', 'Manufacturing order should be confirmed.')
        move = self.StockMove.search([('raw_material_production_id', '=', mnf_product_a.id), ('product_id', '=', product_d.id)])
        self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.")

        # Create inventory for product C.
        # ------------------------------
        # Need product C ( 20 kg + 6 kg + 1500.0 gm = 27.500 kg)
        # -------------------------------------------------------

        self.Quant.with_context(inventory_mode=True).create({
            'product_id': product_c.id, # uom = uom_kg
            'inventory_quantity': 27.51, # round up due to kg.rounding = 0.01
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
        }).action_apply_inventory()

        # Assign product to manufacturing order of product A.
        # ---------------------------------------------------

        mnf_product_a.action_assign()
        self.assertEqual(mnf_product_a.reservation_state, 'assigned', 'Manufacturing order inventory state should be available.')
        moves = self.StockMove.search([('raw_material_production_id', '=', mnf_product_a.id), ('product_id', '=', product_c.id)])

        # Check product c move line state.
        for move in moves:
            self.assertEqual(move.state, 'assigned', "Wrong state in 'To consume line' of manufacturing order.")

        # Produce product A.
        # ------------------

        mo_form = Form(mnf_product_a)
        mo_form.qty_producing = mo_form.product_qty
        mnf_product_a = mo_form.save()
        mnf_product_a._post_inventory()
        # Check state of manufacturing order product A.
        self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should still be in the progress state.')
        # Check product A avaialble quantity should be 120.
        self.assertEqual(product_a.qty_available, 120, 'Wrong quantity available of product A.')

    @skip('Temporary to fast merge new valuation')
    def test_01_sale_mrp_delivery_kit(self):
        """ Test delivered quantity on SO based on delivered quantity in pickings."""
        # intial so
        product = self.env['product.product'].create({
            'name': 'Table Kit',
            'type': 'consu',
            'invoice_policy': 'delivery',
        })
        # Remove the MTO route as purchase is not installed and since the procurement removal the exception is directly raised
        product.write({'route_ids': [(6, 0, [self.company_data['default_warehouse'].manufacture_pull_id.route_id.id])]})

        product_wood_panel = self.env['product.product'].create({
            'name': 'Wood Panel',
            'is_storable': True,
        })
        product_desk_bolt = self.env['product.product'].create({
            'name': 'Bolt',
            'is_storable': True,
        })
        self.env['mrp.bom'].create({
            'product_tmpl_id': product.product_tmpl_id.id,
            'product_uom_id': self.env.ref('uom.product_uom_unit').id,
            'sequence': 2,
            'type': 'phantom',
            'bom_line_ids': [
                (0, 0, {
                    'product_id': product_wood_panel.id,
                    'product_qty': 1,
                    'product_uom_id': self.env.ref('uom.product_uom_unit').id,
                }), (0, 0, {
                    'product_id': product_desk_bolt.id,
                    'product_qty': 4,
                    'product_uom_id': self.env.ref('uom.product_uom_unit').id,
                })
            ]
        })

        partner = self.env['res.partner'].create({'name': 'My Test Partner'})
        # if `delivery` module is installed, a default property is set for the carrier to use
        # However this will lead to an extra line on the SO (the delivery line), which will force
        # the SO to have a different flow (and `invoice_state` value)
        if 'property_delivery_carrier_id' in partner:
            partner.property_delivery_carrier_id = False

        f = Form(self.env['sale.order'])
        f.partner_id = partner
        with f.order_line.new() as line:
            line.product_id = product
            line.product_uom_qty = 5
        so = f.save()

        # confirm our standard so, check the picking
        so.action_confirm()
        self.assertTrue(so.picking_ids, 'Sale MRP: no picking created for "invoice on delivery" storable products')

        # invoice in on delivery, nothing should be invoiced
        with self.assertRaises(UserError):
            so._create_invoices()
        self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "nothing to invoice" after invoicing')

        # deliver partially (1 of each instead of 5), check the so's invoice_status and delivered quantities
        pick = so.picking_ids
        pick.move_ids.write({'quantity': 1, 'picked': True})
        Form.from_action(self.env, pick.button_validate()).save().process()
        self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "no" after partial delivery of a kit')
        del_qty = sum(sol.qty_delivered for sol in so.order_line)
        self.assertEqual(del_qty, 0.0, 'Sale MRP: delivered quantity should be zero after partial delivery of a kit')
        # deliver remaining products, check the so's invoice_status and delivered quantities
        self.assertEqual(len(so.picking_ids), 2, 'Sale MRP: number of pickings should be 2')
        pick_2 = so.picking_ids.filtered('backorder_id')
        for move in pick_2.move_ids:
            if move.product_id.id == product_desk_bolt.id:
                move.write({'quantity': 19, 'picked': True})
            else:
                move.write({'quantity': 4, 'picked': True})
        pick_2.button_validate()

        del_qty = sum(sol.qty_delivered for sol in so.order_line)
        self.assertEqual(del_qty, 5.0, 'Sale MRP: delivered quantity should be 5.0 after complete delivery of a kit')
        self.assertEqual(so.invoice_status, 'to invoice', 'Sale MRP: so invoice_status should be "to invoice" after complete delivery of a kit')

    @skip('Temporary to fast merge new valuation')
    def test_02_sale_mrp_anglo_saxon(self):
        """Test the price unit of a kit"""
        # This test will check that the correct journal entries are created when a stockable product in real time valuation
        # and in fifo cost method is sold in a company using anglo-saxon.
        # For this test, let's consider a product category called Test category in real-time valuation and real price costing method
        # Let's  also consider a finished product with a bom with two components: component1(cost = 20) and component2(cost = 10)
        # These products are in the Test category
        # The bom consists of 2 component1 and 1 component2
        # The invoice policy of the finished product is based on delivered quantities
        self.env.company.currency_id = self.env.ref('base.USD')
        self.uom_unit = self.UoM.create({
            'name': 'Test-Unit',
            'relative_factor': 1,
        })
        self.company = self.company_data['company']
        self.company.anglo_saxon_accounting = True
        self.partner = self.env['res.partner'].create({'name': 'My Test Partner'})
        self.category = self.env.ref('product.product_category_goods').copy({
            'name': 'Test category',
            'property_valuation': 'real_time',
            'property_cost_method': 'fifo',
        })
        self.account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00', 'account_type': 'asset_receivable', 'reconcile': True})
        account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00', 'account_type': 'liability_current', 'reconcile': True})
        account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00', 'account_type': 'asset_current', 'reconcile': True})
        account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'account_type': 'asset_receivable', 'reconcile': True})
        self.partner.property_account_receivable_id = self.account_receiv
        self.category.property_account_income_categ_id = account_income
        self.category.property_account_expense_categ_id = account_expense
        self.category.property_stock_valuation_account_id = account_valuation
        self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'})

        Product = self.env['product.product']
        self.finished_product = Product.create({
                'name': 'Finished product',
                'is_storable': True,
                'uom_id': self.uom_unit.id,
                'invoice_policy': 'delivery',
                'categ_id': self.category.id})
        self.component1 = Product.create({
                'name': 'Component 1',
                'is_storable': True,
                'uom_id': self.uom_unit.id,
                'categ_id': self.category.id,
                'standard_price': 20})
        self.component2 = Product.create({
                'name': 'Component 2',
                'is_storable': True,
                'uom_id': self.uom_unit.id,
                'categ_id': self.category.id,
                'standard_price': 10})

        # Create quants with sudo to avoid:
        # "You are not allowed to create 'Quants' (stock.quant) records. No group currently allows this operation."
        self.env['stock.quant'].sudo().create({
            'product_id': self.component1.id,
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
            'quantity': 6.0,
        })
        self.env['stock.quant'].sudo().create({
            'product_id': self.component2.id,
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
            'quantity': 3.0,
        })
        self.bom = self.env['mrp.bom'].create({
                'product_tmpl_id': self.finished_product.product_tmpl_id.id,
                'product_qty': 1.0,
                'type': 'phantom'})
        BomLine = self.env['mrp.bom.line']
        BomLine.create({
                'product_id': self.component1.id,
                'product_qty': 2.0,
                'bom_id': self.bom.id})
        BomLine.create({
                'product_id': self.component2.id,
                'product_qty': 1.0,
                'bom_id': self.bom.id})

        # Create a SO for a specific partner for three units of the finished product
        so_vals = {
            'partner_id': self.partner.id,
            'partner_invoice_id': self.partner.id,
            'partner_shipping_id': self.partner.id,
            'order_line': [(0, 0, {
                'name': self.finished_product.name,
                'product_id': self.finished_product.id,
                'product_uom_qty': 3,
                'price_unit': self.finished_product.list_price
            })],
            'company_id': self.company.id,
        }
        self.so = self.env['sale.order'].create(so_vals)
        # Validate the SO
        self.so.action_confirm()
        # Deliver the three finished products
        pick = self.so.picking_ids
        # To check the products on the picking
        self.assertEqual(pick.move_ids.mapped('product_id'), self.component1 | self.component2)
        pick.button_validate()
        # Create the invoice
        self.so._create_invoices()
        self.invoice = self.so.invoice_ids
        # Changed the invoiced quantity of the finished product to 2
        move_form = Form(self.invoice)
        with move_form.invoice_line_ids.edit(0) as line_form:
            line_form.quantity = 2.0
        self.invoice = move_form.save()
        self.invoice.action_post()
        aml = self.invoice.line_ids
        aml_expense = aml.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0)
        aml_output = aml.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0)
        # Check that the cost of Good Sold entries are equal to 2* (2 * 20 + 1 * 10) = 100
        self.assertEqual(aml_expense.debit, 100, "Cost of Good Sold entry missing or mismatching")
        self.assertEqual(aml_output.credit, 100, "Cost of Good Sold entry missing or mismatching")

    def test_03_sale_mrp_simple_kit_qty_delivered(self):
        """ Test that the quantities delivered are correct when
        a simple kit is ordered with multiple backorders
        """

        # kit_1 structure:
        # ================

        # kit_1 ---|- component_a  x2
        #          |- component_b  x1
        #          |- component_c  x3

        # Updating the quantities in stock to prevent
        # a 'Not enough inventory' warning message.
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 20)
        self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 30)

        # Creation of a sale order for x10 kit_1
        partner = self.env['res.partner'].create({'name': 'My Test Partner'})
        f = Form(self.env['sale.order'])
        f.partner_id = partner
        with f.order_line.new() as line:
            line.product_id = self.kit_1
            line.product_uom_qty = 10.0

        # Confirming the SO to trigger the picking creation
        so = f.save()
        so.action_confirm()

        # Check picking creation
        self.assertEqual(len(so.picking_ids), 1)
        picking_original = so.picking_ids[0]
        move_ids = picking_original.move_ids

        # Check if the correct amount of stock.moves are created
        self.assertEqual(len(move_ids), 3)

        # Check if BoM is created and is for a 'Kit'
        bom_from_k1 = self.env['mrp.bom']._bom_find(self.kit_1)[self.kit_1]
        self.assertEqual(self.bom_kit_1.id, bom_from_k1.id)
        self.assertEqual(bom_from_k1.type, 'phantom')

        # Check there's only 1 order line on the SO and it's for x10 'kit_1'
        order_lines = so.order_line
        self.assertEqual(len(order_lines), 1)
        order_line = order_lines[0]
        self.assertEqual(order_line.product_id.id, self.kit_1.id)
        self.assertEqual(order_line.product_uom_qty, 10.0)

        # Check if correct qty is ordered for each component of the kit
        expected_quantities = {
            self.component_a: 20,
            self.component_b: 10,
            self.component_c: 30,
        }
        self._assert_quantities(move_ids, expected_quantities)

        # Process only x1 of the first component then create a backorder for the missing components
        picking_original.move_ids.sorted()[0].write({'quantity': 1, 'picked': True})

        Form.from_action(self.env, so.picking_ids[0].button_validate()).save().process()

        # Check that the backorder was created, no kit should be delivered at this point
        self.assertEqual(len(so.picking_ids), 2)
        backorder_1 = so.picking_ids - picking_original
        self.assertEqual(backorder_1.backorder_id.id, picking_original.id)
        self.assertEqual(order_line.qty_delivered, 0)

        # Process only x6 each componenent in the picking
        # Then create a backorder for the missing components
        backorder_1.move_ids.write({'quantity': 6, 'picked': True})
        Form.from_action(self.env, backorder_1.button_validate()).save().process()

        # Check that a backorder is created
        self.assertEqual(len(so.picking_ids), 3)
        backorder_2 = so.picking_ids - picking_original - backorder_1
        self.assertEqual(backorder_2.backorder_id.id, backorder_1.id)

        # With x6 unit of each components, we can only make 2 kits.
        # So only 2 kits should be delivered
        self.assertEqual(order_line.qty_delivered, 2)

        # Process x3 more unit of each components :
        # - Now only 3 kits should be delivered
        # - A backorder will be created, the SO should have 3 picking_ids linked to it.
        backorder_2.move_ids.write({'quantity': 3, 'picked': True})

        Form.from_action(self.env, backorder_2.button_validate()).save().process()

        self.assertEqual(len(so.picking_ids), 4)
        backorder_3 = so.picking_ids - picking_original - backorder_2 - backorder_1
        self.assertEqual(backorder_3.backorder_id.id, backorder_2.id)
        self.assertEqual(order_line.qty_delivered, 3)

        # Adding missing components
        qty_to_process = {
            self.component_a: 10,
            self.component_b: 1,
            self.component_c: 21,
        }
        self._process_quantities(backorder_3.move_ids, qty_to_process)

        # Validating the last backorder now it's complete
        backorder_3.button_validate()
        order_line._compute_qty_delivered()

        # All kits should be delivered
        self.assertEqual(order_line.qty_delivered, 10)

    def test_04_sale_mrp_kit_qty_delivered(self):
        """ Test that the quantities delivered are correct when
        a kit with subkits is ordered with multiple backorders and returns
        """

        # 'kit_parent' structure:
        # ---------------------------
        #
        # kit_parent --|- kit_2 x2 --|- component_d x1
        #              |             |- kit_1 x2 -------|- component_a   x2
        #              |                                |- component_b   x1
        #              |                                |- component_c   x3
        #              |
        #              |- kit_3 x1 --|- component_f x1
        #              |             |- component_g x2
        #              |
        #              |- component_e x1

        # Updating the quantities in stock to prevent
        # a 'Not enough inventory' warning message.
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 56)
        self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 28)
        self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 84)
        self.env['stock.quant']._update_available_quantity(self.component_d, stock_location, 14)
        self.env['stock.quant']._update_available_quantity(self.component_e, stock_location, 7)
        self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 14)
        self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 28)

        # Creation of a sale order for x7 kit_parent
        partner = self.env['res.partner'].create({'name': 'My Test Partner'})
        f = Form(self.env['sale.order'])
        f.partner_id = partner
        with f.order_line.new() as line:
            line.product_id = self.kit_parent
            line.product_uom_qty = 7.0

        so = f.save()
        so.action_confirm()

        # Check picking creation, its move lines should concern
        # only components. Also checks that the quantities are corresponding
        # to the SO
        self.assertEqual(len(so.picking_ids), 1)
        order_line = so.order_line[0]
        picking_original = so.picking_ids[0]
        move_ids = picking_original.move_ids
        products = move_ids.product_id
        kits = [self.kit_parent, self.kit_3, self.kit_2, self.kit_1]
        components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e, self.component_f, self.component_g]
        expected_quantities = {
            self.component_a: 56.0,
            self.component_b: 28.0,
            self.component_c: 84.0,
            self.component_d: 14.0,
            self.component_e: 7.0,
            self.component_f: 14.0,
            self.component_g: 28.0
        }

        self.assertEqual(len(move_ids), 7)
        self.assertTrue(not any(kit in products for kit in kits))
        self.assertTrue(all(component in products for component in components))
        self._assert_quantities(move_ids, expected_quantities)

        # Process only 7 units of each component
        qty_to_process = 7
        move_ids.write({'quantity': qty_to_process, 'picked': True})

        # Create a backorder for the missing componenents
        Form.from_action(self.env, picking_original.button_validate()).save().process()

        # Check that a backorded is created
        self.assertEqual(len(so.picking_ids), 2)
        backorder_1 = so.picking_ids - picking_original
        self.assertEqual(backorder_1.backorder_id.id, picking_original.id)

        # Even if some components are delivered completely,
        # no KitParent should be delivered
        self.assertEqual(order_line.qty_delivered, 0)

        # Process just enough components to make 1 kit_parent
        qty_to_process = {
            self.component_a: 1,
            self.component_c: 5,
        }
        self._process_quantities(backorder_1.move_ids, qty_to_process)

        # Create a backorder for the missing componenents
        Form.from_action(self.env, backorder_1.button_validate()).save().process()

        # Only 1 kit_parent should be delivered at this point
        self.assertEqual(order_line.qty_delivered, 1)

        # Check that the second backorder is created
        self.assertEqual(len(so.picking_ids), 3)
        backorder_2 = so.picking_ids - picking_original - backorder_1
        self.assertEqual(backorder_2.backorder_id.id, backorder_1.id)

        # Set the components quantities that backorder_2 should have
        expected_quantities = {
            self.component_a: 48,
            self.component_b: 21,
            self.component_c: 72,
            self.component_d: 7,
            self.component_f: 7,
            self.component_g: 21
        }

        # Check that the computed quantities are matching the theorical ones.
        # Since component_e was totally processed, this componenent shouldn't be
        # present in backorder_2
        self.assertEqual(len(backorder_2.move_ids), 6)
        move_comp_e = backorder_2.move_ids.filtered(lambda m: m.product_id.id == self.component_e.id)
        self.assertFalse(move_comp_e)
        self._assert_quantities(backorder_2.move_ids, expected_quantities)

        # Process enough components to make x3 kit_parents
        qty_to_process = {
            self.component_a: 16,
            self.component_b: 5,
            self.component_c: 24,
            self.component_g: 5
        }
        self._process_quantities(backorder_2.move_ids, qty_to_process)

        # Create a backorder for the missing componenents
        Form.from_action(self.env, backorder_2.button_validate()).save().process()

        # Check that x3 kit_parents are indeed delivered
        self.assertEqual(order_line.qty_delivered, 3)

        # Check that the third backorder is created
        self.assertEqual(len(so.picking_ids), 4)
        backorder_3 = so.picking_ids - (picking_original + backorder_1 + backorder_2)
        self.assertEqual(backorder_3.backorder_id.id, backorder_2.id)

        # Check the components quantities that backorder_3 should have
        expected_quantities = {
            self.component_a: 32,
            self.component_b: 16,
            self.component_c: 48,
            self.component_d: 7,
            self.component_f: 7,
            self.component_g: 16
        }
        self._assert_quantities(backorder_3.move_ids, expected_quantities)

        # Process all missing components
        self._process_quantities(backorder_3.move_ids, expected_quantities)

        # Validating the last backorder now it's complete.
        # All kits should be delivered
        backorder_3.button_validate()
        self.assertEqual(order_line.qty_delivered, 7.0)

        # Return all components processed by backorder_3
        stock_return_picking_form = Form(self.env['stock.return.picking']
            .with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0],
            active_model='stock.picking'))
        return_wiz = stock_return_picking_form.save()
        for return_move in return_wiz.product_return_moves:
            return_move.write({
                'quantity': expected_quantities[return_move.product_id],
                '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 picking
        return_pick.button_validate()

        # Now quantity delivered should be 3 again
        self.assertEqual(order_line.qty_delivered, 3)

        stock_return_picking_form = Form(self.env['stock.return.picking']
            .with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0],
            active_model='stock.picking'))
        return_wiz = stock_return_picking_form.save()
        for move in return_wiz.product_return_moves:
            move.quantity = expected_quantities[move.product_id]
        res = return_wiz.action_create_returns()
        return_of_return_pick = self.env['stock.picking'].browse(res['res_id'])

        # Process all components except one of each
        for move in return_of_return_pick.move_ids:
            move.write({
                'quantity': expected_quantities[move.product_id] - 1,
                'picked': True,
                'to_refund': True
            })

        Form.from_action(self.env, return_of_return_pick.button_validate()).save().process()

        # As one of each component is missing, only 6 kit_parents should be delivered
        self.assertEqual(order_line.qty_delivered, 6)

        # Check that the 4th backorder is created.
        self.assertEqual(len(so.picking_ids), 7)
        backorder_4 = so.picking_ids - (picking_original + backorder_1 + backorder_2 + backorder_3 + return_of_return_pick + return_pick)
        self.assertEqual(backorder_4.backorder_id.id, return_of_return_pick.id)

        # Check the components quantities that backorder_4 should have
        for move in backorder_4.move_ids:
            self.assertEqual(move.product_qty, 1)

    @mute_logger('odoo.tests.common.onchange')
    def test_05_mrp_sale_kit_availability(self):
        """
        Check that the 'Not enough inventory' warning message shows correct
        informations when a kit is ordered
        """

        warehouse_1 = self.env['stock.warehouse'].create({
            'name': 'Warehouse 1',
            'code': 'WH1'
        })
        warehouse_2 = self.env['stock.warehouse'].create({
            'name': 'Warehouse 2',
            'code': 'WH2'
        })

        # Those are all componenents needed to make kit_parents
        components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e,
                      self.component_f, self.component_g]

        # Set enough quantities to make 1 kit_uom_in_kit in WH1
        self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_1.lot_stock_id, 8)
        self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_1.lot_stock_id, 4)
        self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_1.lot_stock_id, 12)
        self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_1.lot_stock_id, 2)
        self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_1.lot_stock_id, 1)
        self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_1.lot_stock_id, 2)
        self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_1.lot_stock_id, 4)

        # Set quantities on WH2, but not enough to make 1 kit_parent
        self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_2.lot_stock_id, 7)
        self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_2.lot_stock_id, 3)
        self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_2.lot_stock_id, 12)
        self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_2.lot_stock_id, 1)
        self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_2.lot_stock_id, 1)
        self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_2.lot_stock_id, 1)
        self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_2.lot_stock_id, 4)

        # Creation of a sale order for x7 kit_parent
        qty_ordered = 7
        f = Form(self.env['sale.order'])
        f.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        f.warehouse_id = warehouse_2
        with f.order_line.new() as line:
            line.product_id = self.kit_parent
            line.product_uom_qty = qty_ordered
        so = f.save()
        order_line = so.order_line[0]

        # Check that not enough enough quantities are available in the warehouse set in the SO
        # but there are enough quantities in Warehouse 1 for 1 kit_parent
        kit_parent_wh_order = self.kit_parent.with_context(warehouse_id=so.warehouse_id.id)

        # Check that not enough enough quantities are available in the warehouse set in the SO
        # but there are enough quantities in Warehouse 1 for 1 kit_parent
        self.assertEqual(kit_parent_wh_order.virtual_available, 0)
        self.env.invalidate_all()
        kit_parent_wh1 = self.kit_parent.with_context(warehouse_id=warehouse_1.id)
        self.assertEqual(kit_parent_wh1.virtual_available, 1)

        # Check there arn't enough quantities available for the sale order
        self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -1)

        # We receive enoug of each component in Warehouse 2 to make 3 kit_parent
        qty_to_process = {
            self.component_a: (17, self.uom_unit),
            self.component_b: (12, self.uom_unit),
            self.component_c: (25, self.uom_unit),
            self.component_d: (5, self.uom_unit),
            self.component_e: (2, self.uom_unit),
            self.component_f: (5, self.uom_unit),
            self.component_g: (8, self.uom_unit),
        }
        self._create_move_quantities(qty_to_process, components, warehouse_2)

        # As 'Warehouse 2' is the warehouse linked to the SO, 3 kits should be available
        # But the quantity available in Warehouse 1 should stay 1
        kit_parent_wh_order = self.kit_parent.with_context(warehouse_id=so.warehouse_id.id)
        self.assertEqual(kit_parent_wh_order.virtual_available, 3)
        self.env.invalidate_all()
        kit_parent_wh1 = self.kit_parent.with_context(warehouse_id=warehouse_1.id)
        self.assertEqual(kit_parent_wh1.virtual_available, 1)

        # Check there arn't enough quantities available for the sale order
        self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -1)

        # We receive enough of each component in Warehouse 2 to make 7 kit_parent
        qty_to_process = {
            self.component_a: (32, self.uom_unit),
            self.component_b: (16, self.uom_unit),
            self.component_c: (48, self.uom_unit),
            self.component_d: (8, self.uom_unit),
            self.component_e: (4, self.uom_unit),
            self.component_f: (8, self.uom_unit),
            self.component_g: (16, self.uom_unit),
        }
        self._create_move_quantities(qty_to_process, components, warehouse_2)

        # Enough quantities should be available, no warning message should be displayed
        kit_parent_wh_order = self.kit_parent.with_context(warehouse_id=so.warehouse_id.id)
        self.assertEqual(kit_parent_wh_order.virtual_available, 7)

    def test_06_kit_qty_delivered_mixed_uom(self):
        """
        Check that the quantities delivered are correct when a kit involves
        multiple UoMs on its components
        """
        # Create some components
        component_uom_unit = self._cls_create_product('Comp Unit', self.uom_unit)
        component_uom_dozen = self._cls_create_product('Comp Dozen', self.uom_dozen)
        component_uom_kg = self._cls_create_product('Comp Kg', self.uom_kg)

        # Create a kit 'kit_uom_1' :
        # -----------------------
        #
        # kit_uom_1 --|- component_uom_unit    x2 Test-Dozen
        #             |- component_uom_dozen   x1 Test-Dozen
        #             |- component_uom_kg      x3 Test-G

        kit_uom_1 = self._cls_create_product('Kit 1', self.uom_unit)

        bom_kit_uom_1 = self.env['mrp.bom'].create({
            'product_tmpl_id': kit_uom_1.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine = self.env['mrp.bom.line']
        BomLine.create({
            'product_id': component_uom_unit.id,
            'product_qty': 2.0,
            'product_uom_id': self.uom_dozen.id,
            'bom_id': bom_kit_uom_1.id})
        BomLine.create({
            'product_id': component_uom_dozen.id,
            'product_qty': 1.0,
            'product_uom_id': self.uom_dozen.id,
            'bom_id': bom_kit_uom_1.id})
        BomLine.create({
            'product_id': component_uom_kg.id,
            'product_qty': 3.0,
            'product_uom_id': self.uom_gm.id,
            'bom_id': bom_kit_uom_1.id})

        # Updating the quantities in stock to prevent
        # a 'Not enough inventory' warning message.
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(component_uom_unit, stock_location, 240)
        self.env['stock.quant']._update_available_quantity(component_uom_dozen, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(component_uom_kg, stock_location, 0.03)

        # Creation of a sale order for x10 kit_1
        partner = self.env['res.partner'].create({'name': 'My Test Partner'})
        f = Form(self.env['sale.order'])
        f.partner_id = partner
        with f.order_line.new() as line:
            line.product_id = kit_uom_1
            line.product_uom_qty = 10.0

        so = f.save()
        so.action_confirm()

        picking_original = so.picking_ids[0]
        move_ids = picking_original.move_ids
        order_line = so.order_line[0]

        # Check that the quantities on the picking are the one expected for each components
        for move in move_ids:
            corr_bom_line = bom_kit_uom_1.bom_line_ids.filtered(lambda b: b.product_id.id == move.product_id.id)
            computed_qty = move.product_uom._compute_quantity(move.product_uom_qty, corr_bom_line.product_uom_id)
            self.assertEqual(computed_qty, order_line.product_uom_qty * corr_bom_line.product_qty)

        # Processe enough componenents in the picking to make 2 kit_uom_1
        # Then create a backorder for the missing components
        qty_to_process = {
            component_uom_unit: 48,
            component_uom_dozen: 3,
            component_uom_kg: 0.006
        }
        self._process_quantities(move_ids, qty_to_process)
        Form.from_action(self.env, move_ids.picking_id.button_validate()).save().process()

        # Check that a backorder is created
        self.assertEqual(len(so.picking_ids), 2)
        backorder_1 = so.picking_ids - picking_original
        self.assertEqual(backorder_1.backorder_id.id, picking_original.id)

        # Only 2 kits should be delivered
        self.assertEqual(order_line.qty_delivered, 2)

        # Adding missing components
        qty_to_process = {
            component_uom_unit: 192,
            component_uom_dozen: 7,
            component_uom_kg: 0.024
        }
        self._process_quantities(backorder_1.move_ids, qty_to_process)

        # Validating the last backorder now it's complete
        backorder_1.button_validate()
        order_line._compute_qty_delivered()
        # All kits should be delivered
        self.assertEqual(order_line.qty_delivered, 10)

    @mute_logger('odoo.tests.common.onchange')
    def test_07_kit_availability_mixed_uom(self):
        """
        Check that the 'Not enough inventory' warning message displays correct
        informations when a kit with multiple UoMs on its components is ordered
        """

        # Create some components
        component_uom_unit = self._cls_create_product('Comp Unit', self.uom_unit)
        component_uom_dozen = self._cls_create_product('Comp Dozen', self.uom_dozen)
        component_uom_kg = self._cls_create_product('Comp Kg', self.uom_kg)
        component_uom_gm = self._cls_create_product('Comp g', self.uom_gm)
        components = [component_uom_unit, component_uom_dozen, component_uom_kg, component_uom_gm]

        # Create a kit 'kit_uom_in_kit' :
        # -----------------------
        # kit_uom_in_kit --|- component_uom_gm  x3 Test-KG
        #                  |- kit_uom_1         x2 Test-Dozen --|- component_uom_unit    x2 Test-Dozen
        #                                                       |- component_uom_dozen   x1 Test-Dozen
        #                                                       |- component_uom_kg      x5 Test-G

        kit_uom_1 = self._cls_create_product('Sub Kit 1', self.uom_unit)
        kit_uom_in_kit = self._cls_create_product('Parent Kit', self.uom_unit)

        bom_kit_uom_1 = self.env['mrp.bom'].create({
            'product_tmpl_id': kit_uom_1.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine = self.env['mrp.bom.line']
        BomLine.create({
            'product_id': component_uom_unit.id,
            'product_qty': 2.0,
            'product_uom_id': self.uom_dozen.id,
            'bom_id': bom_kit_uom_1.id})
        BomLine.create({
            'product_id': component_uom_dozen.id,
            'product_qty': 1.0,
            'product_uom_id': self.uom_dozen.id,
            'bom_id': bom_kit_uom_1.id})
        BomLine.create({
            'product_id': component_uom_kg.id,
            'product_qty': 5.0,
            'product_uom_id': self.uom_gm.id,
            'bom_id': bom_kit_uom_1.id})

        bom_kit_uom_in_kit = self.env['mrp.bom'].create({
            'product_tmpl_id': kit_uom_in_kit.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom'})

        BomLine.create({
            'product_id': component_uom_gm.id,
            'product_qty': 3.0,
            'product_uom_id': self.uom_kg.id,
            'bom_id': bom_kit_uom_in_kit.id})
        BomLine.create({
            'product_id': kit_uom_1.id,
            'product_qty': 2.0,
            'product_uom_id': self.uom_dozen.id,
            'bom_id': bom_kit_uom_in_kit.id})

        # Create a simple warehouse to receives some products
        warehouse_1 = self.env['stock.warehouse'].create({
            'name': 'Warehouse 1',
            'code': 'WH1'
        })

        # Set enough quantities to make 1 kit_uom_in_kit in WH1
        self.env['stock.quant']._update_available_quantity(component_uom_unit, warehouse_1.lot_stock_id, 576)
        self.env['stock.quant']._update_available_quantity(component_uom_dozen, warehouse_1.lot_stock_id, 24)
        self.env['stock.quant']._update_available_quantity(component_uom_kg, warehouse_1.lot_stock_id, 0.12)
        self.env['stock.quant']._update_available_quantity(component_uom_gm, warehouse_1.lot_stock_id, 3000)

        # Creation of a sale order for x5 kit_uom_in_kit
        qty_ordered = 5
        f = Form(self.env['sale.order'])
        f.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        f.warehouse_id = warehouse_1
        with f.order_line.new() as line:
            line.product_id = kit_uom_in_kit
            line.product_uom_qty = qty_ordered

        so = f.save()
        order_line = so.order_line[0]

        # Check that not enough enough quantities are available in the warehouse set in the SO
        # but there are enough quantities in Warehouse 1 for 1 kit_parent
        kit_uom_in_kit.with_context(warehouse_id=warehouse_1.id)._compute_quantities()
        virtual_available_wh_order = kit_uom_in_kit.virtual_available
        self.assertEqual(virtual_available_wh_order, 1)

        # Check there arn't enough quantities available for the sale order
        self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -1)

        # We receive enough of each component in Warehouse 1 to make 3 kit_uom_in_kit.
        # Moves are created instead of only updating the quant quantities in order to trigger every compute fields.
        qty_to_process = {
            component_uom_unit: (1152, self.uom_unit),
            component_uom_dozen: (48, self.uom_dozen),
            component_uom_kg: (0.24, self.uom_kg),
            component_uom_gm: (6000, self.uom_gm)
        }
        self._create_move_quantities(qty_to_process, components, warehouse_1)

        # Check there arn't enough quantities available for the sale order
        self.assertTrue(line.product_uom_id.compare(order_line.virtual_available_at_date - order_line.product_uom_qty, 0) == -1)
        kit_uom_in_kit.with_context(warehouse_id=warehouse_1.id)._compute_quantities()
        virtual_available_wh_order = kit_uom_in_kit.virtual_available
        self.assertEqual(virtual_available_wh_order, 3)

        # We process enough quantities to have enough kit_uom_in_kit available for the sale order.
        self._create_move_quantities(qty_to_process, components, warehouse_1)

        # We check that enough quantities were processed to sell 5 kit_uom_in_kit
        kit_uom_in_kit.with_context(warehouse_id=warehouse_1.id)._compute_quantities()
        self.assertEqual(kit_uom_in_kit.virtual_available, 5)

    def test_10_sale_mrp_kits_routes(self):

        # Create a kit 'kit_1' :
        # -----------------------
        #
        # kit_1 --|- component_shelf1   x3
        #         |- component_shelf2   x2

        stock_shelf_1 = self.env['stock.location'].create({
            'name': 'Shelf 1',
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
        })
        stock_shelf_2 = self.env['stock.location'].create({
            'name': 'Shelf 2',
            'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
        })

        kit_1 = self._cls_create_product('Kit1', self.uom_unit)
        component_shelf1 = self._cls_create_product('Comp Shelf1', self.uom_unit)
        component_shelf2 = self._cls_create_product('Comp Shelf2', self.uom_unit)

        with Form(self.env['mrp.bom']) as bom:
            bom.product_tmpl_id = kit_1.product_tmpl_id
            bom.product_qty = 1
            bom.product_uom_id = self.uom_unit
            bom.type = 'phantom'
            with bom.bom_line_ids.new() as line:
                line.product_id = component_shelf1
                line.product_qty = 3
                line.product_uom_id = self.uom_unit
            with bom.bom_line_ids.new() as line:
                line.product_id = component_shelf2
                line.product_qty = 2
                line.product_uom_id = self.uom_unit

        # Creating 2 specific routes for each of the components of the kit
        route_shelf1 = self.env['stock.route'].create({
            'name': 'Shelf1 -> Customer',
            'product_selectable': True,
            'rule_ids': [(0, 0, {
                'name': 'Shelf1 -> Customer',
                'action': 'pull',
                'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
                'location_src_id': stock_shelf_1.id,
                'location_dest_id': self.ref('stock.stock_location_customers'),
            })],
        })

        route_shelf2 = self.env['stock.route'].create({
            'name': 'Shelf2 -> Customer',
            'product_selectable': True,
            'rule_ids': [(0, 0, {
                'name': 'Shelf2 -> Customer',
                'action': 'pull',
                'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
                'location_src_id': stock_shelf_2.id,
                'location_dest_id': self.ref('stock.stock_location_customers'),
            })],
        })

        component_shelf1.write({
            'route_ids': [(4, route_shelf1.id)]})
        component_shelf2.write({
            'route_ids': [(4, route_shelf2.id)]})

        # Set enough quantities to make 1 kit_uom_in_kit in WH1
        self.env['stock.quant']._update_available_quantity(component_shelf1, self.company_data['default_warehouse'].lot_stock_id, 15)
        self.env['stock.quant']._update_available_quantity(component_shelf2, self.company_data['default_warehouse'].lot_stock_id, 10)

        # Creating a sale order for 5 kits and confirming it
        order_form = Form(self.env['sale.order'])
        order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        with order_form.order_line.new() as line:
            line.product_id = kit_1
            line.product_uom_qty = 5
        order = order_form.save()
        order.action_confirm()

        # Now we check that the routes of the components were applied, in order to make sure the routes set
        # on the kit itself are ignored
        self.assertEqual(len(order.picking_ids), 2)
        self.assertEqual(len(order.picking_ids[0].move_ids), 1)
        self.assertEqual(len(order.picking_ids[1].move_ids), 1)
        moves = order.picking_ids.move_ids
        move_shelf1 = moves.filtered(lambda m: m.product_id == component_shelf1)
        move_shelf2 = moves.filtered(lambda m: m.product_id == component_shelf2)
        self.assertEqual(move_shelf1.location_id.id, stock_shelf_1.id)
        self.assertEqual(move_shelf1.location_dest_id.id, self.ref('stock.stock_location_customers'))
        self.assertEqual(move_shelf2.location_id.id, stock_shelf_2.id)
        self.assertEqual(move_shelf2.location_dest_id.id, self.ref('stock.stock_location_customers'))

    def test_11_sale_mrp_explode_kits_uom_quantities(self):

        # Create a kit 'kit_1' :
        # -----------------------
        #
        # 2x Dozens kit_1 --|- component_unit   x6 Units
        #                   |- component_kg     x7 Kg

        kit_1 = self._cls_create_product('Kit1', self.uom_unit)
        component_unit = self._cls_create_product('Comp Unit', self.uom_unit)
        component_kg = self._cls_create_product('Comp Kg', self.uom_kg)

        with Form(self.env['mrp.bom']) as bom:
            bom.product_tmpl_id = kit_1.product_tmpl_id
            bom.product_qty = 2
            bom.product_uom_id = self.uom_dozen
            bom.type = 'phantom'
            with bom.bom_line_ids.new() as line:
                line.product_id = component_unit
                line.product_qty = 6
                line.product_uom_id = self.uom_unit
            with bom.bom_line_ids.new() as line:
                line.product_id = component_kg
                line.product_qty = 7
                line.product_uom_id = self.uom_kg

        # Create a simple warehouse to receives some products
        warehouse_1 = self.env['stock.warehouse'].create({
            'name': 'Warehouse 1',
            'code': 'WH1'
        })
        # Set enough quantities to make 1 Test-Dozen kit_uom_in_kit
        self.env['stock.quant']._update_available_quantity(component_unit, warehouse_1.lot_stock_id, 12)
        self.env['stock.quant']._update_available_quantity(component_kg, warehouse_1.lot_stock_id, 14)

        # Creating a sale order for 3 Units of kit_1 and confirming it
        order_form = Form(self.env['sale.order'])
        order_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        order_form.warehouse_id = warehouse_1
        with order_form.order_line.new() as line:
            line.product_id = kit_1
            line.product_uom_qty = 2
        order = order_form.save()
        order.action_confirm()

        # Now we check that the routes of the components were applied, in order to make sure the routes set
        # on the kit itself are ignored
        self.assertEqual(len(order.picking_ids), 1)
        self.assertEqual(len(order.picking_ids[0].move_ids), 2)

        # Finally, we check the quantities for each component on the picking
        move_component_unit = order.picking_ids[0].move_ids.filtered(lambda m: m.product_id == component_unit)
        move_component_kg = order.picking_ids[0].move_ids - move_component_unit
        self.assertEqual(move_component_unit.product_uom_qty, 0.5)
        self.assertEqual(move_component_kg.product_uom_qty, 0.59)

    def test_product_type_service_1(self):
        route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id.id
        route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id.id
        self.uom_unit = self.env.ref('uom.product_uom_unit')

        # Create finished product
        finished_product = self.env['product.product'].create({
            'name': 'Geyser',
            'is_storable': True,
            'route_ids': [(4, route_mto), (4, route_manufacture)],
        })

        # Create service type product
        product_raw = self.env['product.product'].create({
            'name': 'raw Geyser',
            'type': 'service',
        })

        # Create bom for finish product
        bom = self.env['mrp.bom'].create({
            'product_id': finished_product.id,
            'product_tmpl_id': finished_product.product_tmpl_id.id,
            'product_uom_id': self.env.ref('uom.product_uom_unit').id,
            'product_qty': 1.0,
            'type': 'normal',
            'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
        })

        # Create sale order
        sale_form = Form(self.env['sale.order'])
        sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        with sale_form.order_line.new() as line:
            line.name = finished_product.name
            line.product_id = finished_product
            line.product_uom_qty = 1.0
            line.price_unit = 10.0
        sale_order = sale_form.save()

        sale_order.action_confirm()

        mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])

        self.assertTrue(mo, 'Manufacturing order created.')

    def test_cancel_flow_1(self):
        """ Sell a MTO/manufacture product.

        Cancel the delivery and the production order. Then duplicate
        the delivery. Another production order should be created."""
        route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id
        route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id
        route_mto.rule_ids.procure_method = "make_to_order"
        self.uom_unit = self.env.ref('uom.product_uom_unit')

        # Create finished product
        finished_product = self.env['product.product'].create({
            'name': 'Geyser',
            'is_storable': True,
            'route_ids': [(4, route_mto.id), (4, route_manufacture.id)],
        })

        product_raw = self.env['product.product'].create({
            'name': 'raw Geyser',
            'is_storable': True,
        })

        # Create bom for finish product
        bom = self.env['mrp.bom'].create({
            'product_id': finished_product.id,
            'product_tmpl_id': finished_product.product_tmpl_id.id,
            'product_uom_id': self.env.ref('uom.product_uom_unit').id,
            'product_qty': 1.0,
            'type': 'normal',
            'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
        })

        # Create sale order
        sale_form = Form(self.env['sale.order'])
        sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        with sale_form.order_line.new() as line:
            line.name = finished_product.name
            line.product_id = finished_product
            line.product_uom_qty = 1.0
            line.price_unit = 10.0
        sale_order = sale_form.save()

        sale_order.action_confirm()

        mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
        delivery = sale_order.picking_ids
        delivery.action_cancel()
        mo.action_cancel()
        copied_delivery = delivery.copy()
        copied_delivery.action_confirm()
        mos = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
        self.assertEqual(len(mos), 1)
        self.assertEqual(mos.state, 'cancel')

    def test_cancel_flow_2(self):
        """ Sell a MTO/manufacture product.

        Cancel the production order and the delivery. Then duplicate
        the delivery. Another production order should be created."""
        route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id
        route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id
        route_mto.rule_ids.procure_method = "make_to_order"
        self.uom_unit = self.env.ref('uom.product_uom_unit')

        # Create finished product
        finished_product = self.env['product.product'].create({
            'name': 'Geyser',
            'is_storable': True,
            'route_ids': [(4, route_mto.id), (4, route_manufacture.id)],
        })

        product_raw = self.env['product.product'].create({
            'name': 'raw Geyser',
            'is_storable': True,
        })

        # Create bom for finish product
        bom = self.env['mrp.bom'].create({
            'product_id': finished_product.id,
            'product_tmpl_id': finished_product.product_tmpl_id.id,
            'product_uom_id': self.env.ref('uom.product_uom_unit').id,
            'product_qty': 1.0,
            'type': 'normal',
            'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
        })

        # Create sale order
        sale_form = Form(self.env['sale.order'])
        sale_form.partner_id = self.env['res.partner'].create({'name': 'My Test Partner'})
        with sale_form.order_line.new() as line:
            line.name = finished_product.name
            line.product_id = finished_product
            line.product_uom_qty = 1.0
            line.price_unit = 10.0
        sale_order = sale_form.save()

        sale_order.action_confirm()

        mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
        delivery = sale_order.picking_ids
        mo.action_cancel()
        delivery.action_cancel()
        copied_delivery = delivery.copy()
        copied_delivery.action_confirm()
        mos = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
        self.assertEqual(len(mos), 1)
        self.assertEqual(mos.state, 'cancel')

    def test_13_so_return_kit(self):
        """
        Test that when returning a SO containing only a kit that contains another kit, the
        SO delivered quantities is set to 0 (with the all-or-nothing policy).
        Products :
            Main Kit
            Nested Kit
            Screw
        BoMs :
            Main Kit BoM (kit), recipe :
                Nested Kit Bom (kit), recipe :
                    Screw
        Business flow :
            Create those
            Create a Sales order selling one Main Kit BoM
            Confirm the sales order
            Validate the delivery (outgoing) (qty_delivered = 1)
            Create a return for the delivery
            Validate return for delivery (ingoing) (qty_delivered = 0)
        """
        main_kit_product = self.env['product.product'].create({
            'name': 'Main Kit',
            'is_storable': True,
        })

        nested_kit_product = self.env['product.product'].create({
            'name': 'Nested Kit',
            'is_storable': True,
        })

        product = self.env['product.product'].create({
            'name': 'Screw',
            'is_storable': True,
        })

        self.env['mrp.bom'].create({
            'product_id': nested_kit_product.id,
            'product_tmpl_id': nested_kit_product.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(5, 0), (0, 0, {'product_id': product.id})]
        })

        self.env['mrp.bom'].create({
            'product_id': main_kit_product.id,
            'product_tmpl_id': main_kit_product.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(5, 0), (0, 0, {'product_id': nested_kit_product.id})]
        })

        # Create a SO for product Main Kit Product
        order_form = Form(self.env['sale.order'])
        order_form.partner_id = self.env['res.partner'].create({'name': 'Test Partner'})
        with order_form.order_line.new() as line:
            line.product_id = main_kit_product
            line.product_uom_qty = 1
        order = order_form.save()
        order.action_confirm()
        qty_del_not_yet_validated = sum(sol.qty_delivered for sol in order.order_line)
        self.assertEqual(qty_del_not_yet_validated, 0.0, 'No delivery validated yet')

        # Validate delivery
        pick = order.picking_ids
        pick.move_ids.write({'quantity': 1, 'picked': True})
        pick.button_validate()
        qty_del_validated = sum(sol.qty_delivered for sol in order.order_line)
        self.assertEqual(qty_del_validated, 1.0, 'The order went from warehouse to client, so it has been delivered')

        # 1 was delivered, now create a return
        stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
            active_ids=pick.ids, active_id=pick.ids[0], active_model='stock.picking'))
        return_wiz = stock_return_picking_form.save()
        for return_move in return_wiz.product_return_moves:
            return_move.write({
                'quantity': 1,
                'to_refund': True
            })
        res = return_wiz.action_create_returns()
        return_pick = self.env['stock.picking'].browse(res['res_id'])
        return_pick.move_line_ids.quantity = 1
        return_pick.button_validate()  # validate return

        # Delivered quantities to the client should be 0
        qty_del_return_validated = sum(sol.qty_delivered for sol in order.order_line)
        self.assertNotEqual(qty_del_return_validated, 1.0, "The return was validated, therefore the delivery from client to"
                                                           " company was successful, and the client is left without his 1 product.")
        self.assertEqual(qty_del_return_validated, 0.0, "The return has processed, client doesn't have any quantity anymore")

    def test_14_change_bom_type(self):
        """ This test ensures that updating a Bom type during a flow does not lead to any error """
        p1 = self._cls_create_product('Master', self.uom_unit)
        p2 = self._cls_create_product('Component', self.uom_unit)
        p3 = self.component_a
        p1.categ_id.write({
            'property_cost_method': 'average',
            'property_valuation': 'real_time',
        })
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1)

        self.env['mrp.bom'].create({
            'product_tmpl_id': p1.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(0, 0, {
                'product_id': p2.id,
                'product_qty': 1.0,
            })]
        })

        p2_bom = self.env['mrp.bom'].create({
            'product_tmpl_id': p2.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(0, 0, {
                'product_id': p3.id,
                'product_qty': 1.0,
            })]
        })

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.env['res.partner'].create({'name': 'Super Partner'})
        with so_form.order_line.new() as so_line:
            so_line.product_id = p1
        so = so_form.save()
        so.action_confirm()

        so.picking_ids.button_validate()

        p2_bom.type = "normal"

        so._create_invoices()
        invoice = so.invoice_ids
        invoice.action_post()
        self.assertEqual(invoice.state, 'posted')

    @skip('Temporary to fast merge new valuation')
    def test_15_anglo_saxon_variant_price_unit(self):
        """
        Test the price unit of a variant from which template has another variant with kit bom.
        Products:
            Template A
                variant NOKIT
                variant KIT:
                    Component A
        Business Flow:
            create products and kit
            create SO selling both variants
            validate the delivery
            create the invoice
            post the invoice
        """

        # Create environment
        self.env.company.currency_id = self.env.ref('base.USD')
        self.env.company.anglo_saxon_accounting = True
        self.partner = self.env['res.partner'].create({'name': 'Test Partner'})
        self.category = self.env.ref('product.product_category_goods').copy({
            'name': 'Test category',
            'property_valuation': 'real_time',
            'property_cost_method': 'fifo',
        })
        self.stock_location = self.company_data['default_warehouse'].lot_stock_id

        # Create variant attributes
        self.prod_att_test = self.env['product.attribute'].create({'name': 'test'})
        self.prod_attr_KIT = self.env['product.attribute.value'].create({'name': 'KIT', 'attribute_id': self.prod_att_test.id, 'sequence': 1})
        self.prod_attr_NOKIT = self.env['product.attribute.value'].create({'name': 'NOKIT', 'attribute_id': self.prod_att_test.id, 'sequence': 2})

        # Create the template
        self.product_template = self.env['product.template'].create({
            'name': 'Template A',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'invoice_policy': 'delivery',
            'categ_id': self.category.id,
            'attribute_line_ids': [(0, 0, {
                'attribute_id': self.prod_att_test.id,
                'value_ids': [(6, 0, [self.prod_attr_KIT.id, self.prod_attr_NOKIT.id])]
            })]
        })

        # Create the variants
        self.pt_attr_KIT = self.product_template.attribute_line_ids[0].product_template_value_ids[0]
        self.pt_attr_NOKIT = self.product_template.attribute_line_ids[0].product_template_value_ids[1]
        self.variant_KIT = self.product_template._get_variant_for_combination(self.pt_attr_KIT)
        self.variant_NOKIT = self.product_template._get_variant_for_combination(self.pt_attr_NOKIT)
        # Assign a cost to the NOKIT variant
        self.variant_NOKIT.write({'standard_price': 25})

        # Create the components
        self.comp_kit_a = self.env['product.product'].create({
            'name': 'Component Kit A',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'categ_id': self.category.id,
            'standard_price': 20
        })
        self.comp_kit_b = self.env['product.product'].create({
            'name': 'Component Kit B',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'categ_id': self.category.id,
            'standard_price': 10
        })

        # Create the bom
        bom = self.env['mrp.bom'].create({
            'product_tmpl_id': self.product_template.id,
            'product_id': self.variant_KIT.id,
            'product_qty': 1.0,
            'type': 'phantom'
        })
        self.env['mrp.bom.line'].create({
            'product_id': self.comp_kit_a.id,
            'product_qty': 2.0,
            'bom_id': bom.id
        })
        self.env['mrp.bom.line'].create({
            'product_id': self.comp_kit_b.id,
            'product_qty': 1.0,
            'bom_id': bom.id
        })

        # Create the quants
        self.env['stock.quant']._update_available_quantity(self.comp_kit_a, self.stock_location, 2)
        self.env['stock.quant']._update_available_quantity(self.comp_kit_b, self.stock_location, 1)
        self.env['stock.quant']._update_available_quantity(self.variant_NOKIT, self.stock_location, 1)

        # Create the sale order
        so_vals = {
            'partner_id': self.partner.id,
            'partner_invoice_id': self.partner.id,
            'partner_shipping_id': self.partner.id,
            'order_line': [(0, 0, {
                'name': self.variant_KIT.name,
                'product_id': self.variant_KIT.id,
                'product_uom_qty': 1,
                'price_unit': 100,
            }), (0, 0, {
                'name': self.variant_NOKIT.name,
                'product_id': self.variant_NOKIT.id,
                'product_uom_qty': 1,
                'price_unit': 50
            })],
            'company_id': self.env.company.id
        }
        so = self.env['sale.order'].create(so_vals)
        # Validate the sale order
        so.action_confirm()
        # Deliver the products
        pick = so.picking_ids
        pick.button_validate()
        # Create the invoice
        so._create_invoices()
        # Validate the invoice
        invoice = so.invoice_ids
        invoice.action_post()

        amls = invoice.line_ids
        aml_kit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_KIT)
        aml_kit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_KIT)
        aml_nokit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_NOKIT)
        aml_nokit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_NOKIT)

        # Check that the Cost of Goods Sold for variant KIT is equal to 2*(2*20)+10 = 90
        self.assertEqual(aml_kit_expense.debit, 90, "Cost of Good Sold entry missing or mismatching for variant with kit")
        self.assertEqual(aml_kit_output.credit, 90, "Cost of Good Sold entry missing or mismatching for variant with kit")
        # Check that the Cost of Goods Sold for variant NOKIT is equal to its standard_price = 25
        self.assertEqual(aml_nokit_expense.debit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit")
        self.assertEqual(aml_nokit_output.credit, 25, "Cost of Good Sold entry missing or mismatching for variant without kit")

    @skip('Temporary to fast merge new valuation')
    def test_16_anglo_saxon_variant_price_unit_multi_company(self):
        """
        Test the price unit of the BOM of the stock move is taken
        Products:
            Template A
                variant KIT 1
                variant KIT 2
        Business Flow:
            create SO
            validate the delivery
            (cannot archive the BOM, because it is used in a non-invoiced line)
            update the BOM and create a new one
            create the invoice
            post the invoice
        """

        # Create environment
        self.partner = self.env['res.partner'].create({'name': 'Test Partner'})
        self.category = self.env.ref('product.product_category_goods').copy({
            'name': 'Test category',
            'property_valuation': 'real_time',
            'property_cost_method': 'fifo',
        })
        account_receiv = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00', 'account_type': 'asset_receivable', 'reconcile': True})
        account_income = self.env['account.account'].create({'name': 'Income', 'code': 'INC00', 'account_type': 'asset_current', 'reconcile': True})
        account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00', 'account_type': 'liability_current', 'reconcile': True})
        account_valuation = self.env['account.account'].create({'name': 'Valuation', 'code': 'STV00', 'account_type': 'asset_receivable', 'reconcile': True})
        self.stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.partner.property_account_receivable_id = account_receiv
        self.category.property_account_income_categ_id = account_income
        self.category.property_account_expense_categ_id = account_expense
        self.category.property_stock_account_input_categ_id = account_income
        self.category.property_stock_valuation_account_id = account_valuation

        # Create variant attributes
        self.prod_att_test = self.env['product.attribute'].create({'name': 'test'})
        self.prod_attr_KIT_A = self.env['product.attribute.value'].create({'name': 'KIT A', 'attribute_id': self.prod_att_test.id, 'sequence': 1})

        # Create the template
        self.product_template = self.env['product.template'].create({
            'name': 'Template A',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'invoice_policy': 'delivery',
            'categ_id': self.category.id,
            'attribute_line_ids': [(0, 0, {
                'attribute_id': self.prod_att_test.id,
                'value_ids': [(6, 0, [self.prod_attr_KIT_A.id])]
            })]
        })

        # Create another variant
        self.pt_attr_KIT_A = self.product_template.attribute_line_ids[0].product_template_value_ids[0]
        self.variant_KIT_A = self.product_template._get_variant_for_combination(self.pt_attr_KIT_A)
        # Assign a cost to the NOKIT variant
        self.variant_KIT_A.write({'standard_price': 25})

        # Create the components
        self.comp_kit_a = self.env['product.product'].create({
            'name': 'Component Kit A',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'categ_id': self.category.id,
            'standard_price': 20
        })
        self.comp_kit_b = self.env['product.product'].create({
            'name': 'Component Kit B',
            'is_storable': True,
            'uom_id': self.uom_unit.id,
            'categ_id': self.category.id,
            'standard_price': 10
        })

        # Create the bom
        bom = self.env['mrp.bom'].create({
            'product_tmpl_id': self.product_template.id,
            'product_id': self.variant_KIT_A.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'company_id': self.env.company.id,
        })
        self.env['mrp.bom.line'].create({
            'product_id': self.comp_kit_a.id,
            'product_qty': 1.0,
            'company_id': self.env.company.id,
            'bom_id': bom.id
        })

        # Create the quants
        self.env['stock.quant']._update_available_quantity(self.comp_kit_a, self.stock_location, 2)
        self.env['stock.quant']._update_available_quantity(self.comp_kit_b, self.stock_location, 1)

        # Create the sale order
        so_vals = {
            'partner_id': self.partner.id,
            'partner_invoice_id': self.partner.id,
            'partner_shipping_id': self.partner.id,
            'order_line': [(0, 0, {
                'name': self.variant_KIT_A.name,
                'product_id': self.variant_KIT_A.id,
                'product_uom_qty': 1,
                'price_unit': 50
            })],
            'company_id': self.env.company.id,
        }
        so = self.env['sale.order'].create(so_vals)
        # Validate the sale order
        so.action_confirm()
        # Deliver the products
        pick = so.picking_ids
        pick.button_validate()
        # Update BOM
        bom_updated = self.env['mrp.bom'].create({
            'product_tmpl_id': self.product_template.id,
            'product_id': self.variant_KIT_A.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'company_id': self.env.company.id,
        })
        self.env['mrp.bom.line'].create({
            'product_id': self.comp_kit_b.id,
            'product_qty': 1.0,
            'company_id': self.env.company.id,
            'bom_id': bom_updated.id
        })

        # Create the invoice
        so._create_invoices()
        # Validate the invoice
        invoice = so.invoice_ids
        invoice.action_post()

        amls = invoice.line_ids
        aml_nokit_expense = amls.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0 and l.product_id == self.variant_KIT_A)
        aml_nokit_output = amls.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0 and l.product_id == self.variant_KIT_A)

        # Check that the Cost of Goods Sold for variant NOKIT is equal to the cost of the first BOM
        self.assertEqual(aml_nokit_expense.debit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit")
        self.assertEqual(aml_nokit_output.credit, 20, "Cost of Good Sold entry missing or mismatching for variant without kit")

    def test_reconfirm_cancelled_kit(self):
        so = self.env['sale.order'].create({
            'partner_id': self.env['res.partner'].create({'name': 'Test Partner'}).id,
            'order_line': [
                (0, 0, {
                    'name': self.kit_1.name,
                    'product_id': self.kit_1.id,
                    'product_uom_qty': 1.0,
                    'price_unit': 1.0,
                })
            ],
        })

        # Updating the quantities in stock to prevent a 'Not enough inventory' warning message.
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 10)

        so.action_confirm()
        # Check picking creation
        self.assertEqual(len(so.picking_ids), 1, "A picking should be created after the SO validation")

        so.picking_ids.button_validate()

        so._action_cancel()
        so.action_draft()
        so.action_confirm()
        self.assertEqual(len(so.picking_ids), 1, "The product was already delivered, no need to re-create a delivery order")

    @skip('Temporary to fast merge new valuation')
    def test_kit_margin_and_return_picking(self):
        """ This test ensure that, when returning the components of a sold kit, the
        sale order line cost does not change"""
        kit = self._cls_create_product('Super Kit', self.uom_unit)
        (kit + self.component_a).categ_id.property_cost_method = 'fifo'

        self.env['mrp.bom'].create({
            'product_tmpl_id': kit.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(0, 0, {
                'product_id': self.component_a.id,
                'product_qty': 1.0,
            })]
        })

        self.component_a.standard_price = 10
        kit.button_bom_cost()

        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 1)

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner_a
        with so_form.order_line.new() as line:
            line.product_id = kit
        so = so_form.save()
        so.action_confirm()

        line = so.order_line
        price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
        self.assertEqual(price, 10)

        picking = so.picking_ids
        picking.button_validate()

        ctx = {'active_ids':picking.ids, 'active_id': picking.ids[0], 'active_model': 'stock.picking'}
        return_picking_wizard_form = Form(self.env['stock.return.picking'].with_context(ctx))
        return_picking_wizard = return_picking_wizard_form.save()
        return_picking_wizard.product_return_moves.quantity = 1
        return_picking_wizard.action_create_returns()

        price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
        self.assertEqual(price, 10)

    def test_kit_decrease_sol_qty(self):
        """
        Create and confirm a SO with a qty. Increasing/Decreasing the SOL qty
        should update the qty on the delivery. Then, process the delivery, make
        a return and adapt the SOL qty -> there should not be any new picking
        """
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        custo_location = self.env.ref('stock.stock_location_customers')

        grp_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'group_ids': [(4, grp_uom.id)]})

        # 100 kit_3 = 100 x compo_f + 200 x compo_g
        self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 100)
        self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 200)

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner_a
        with so_form.order_line.new() as line:
            line.product_id = self.kit_3
            line.product_uom_qty = 7
            line.product_uom_id = self.uom_ten
        so = so_form.save()
        so.action_confirm()

        delivery = so.picking_ids
        self.assertRecordValues(delivery.move_ids, [
            {'product_id': self.component_f.id, 'product_uom_qty': 70},
            {'product_id': self.component_g.id, 'product_uom_qty': 140},
        ])

        # Decrease
        with Form(so) as so_form:
            with so_form.order_line.edit(0) as line:
                line.product_uom_qty = 6
        self.assertRecordValues(delivery.move_ids, [
            {'product_id': self.component_f.id, 'product_uom_qty': 60},
            {'product_id': self.component_g.id, 'product_uom_qty': 120},
        ])

        # Increase
        with Form(so) as so_form:
            with so_form.order_line.edit(0) as line:
                line.product_uom_qty = 10
        self.assertRecordValues(delivery.move_ids, [
            {'product_id': self.component_f.id, 'product_uom_qty': 100},
            {'product_id': self.component_g.id, 'product_uom_qty': 200},
        ])
        delivery.button_validate()

        # Return 2 [uom_ten] x kit_3
        return_wizard_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking'))
        return_wizard = return_wizard_form.save()
        return_wizard.product_return_moves[0].quantity = 20
        return_wizard.product_return_moves[1].quantity = 40
        action = return_wizard.action_create_returns()
        return_picking = self.env['stock.picking'].browse(action['res_id'])
        return_picking.move_ids.picked = True
        return_picking.button_validate()

        # Adapt the SOL qty according to the delivered one
        with Form(so) as so_form:
            with so_form.order_line.edit(0) as line:
                line.product_uom_qty = 8

        self.assertRecordValues(so.picking_ids.sorted('id').move_ids, [
            {'product_id': self.component_f.id, 'location_dest_id': custo_location.id, 'quantity': 100, 'state': 'done'},
            {'product_id': self.component_g.id, 'location_dest_id': custo_location.id, 'quantity': 200, 'state': 'done'},
            {'product_id': self.component_f.id, 'location_dest_id': stock_location.id, 'quantity': 20, 'state': 'done'},
            {'product_id': self.component_g.id, 'location_dest_id': stock_location.id, 'quantity': 40, 'state': 'done'},
        ])

    def test_kit_decrease_sol_qty_to_zero(self):
        """
        Create and confirm a SO with a kit product. Increasing/Decreasing the SOL qty
        should update the qty on the delivery.
        """
        stock_location = self.company_data['default_warehouse'].lot_stock_id

        grp_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'group_ids': [(4, grp_uom.id)]})

        # 10 kit_3 = 10 x compo_f + 20 x compo_g
        self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 20)

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner_a
        with so_form.order_line.new() as line:
            line.product_id = self.kit_3
            line.product_uom_qty = 2
            line.product_uom_id = self.uom_ten
        so = so_form.save()
        so.action_confirm()

        delivery = so.picking_ids
        self.assertRecordValues(delivery.move_ids, [
            {'product_id': self.component_f.id, 'product_uom_qty': 20},
            {'product_id': self.component_g.id, 'product_uom_qty': 40},
        ])

        # Decrease the qty to 0
        with Form(so) as so_form:
            with so_form.order_line.edit(0) as line:
                line.product_uom_qty = 0
        self.assertRecordValues(delivery.move_ids, [
            {'product_id': self.component_f.id, 'product_uom_qty': 0},
            {'product_id': self.component_g.id, 'product_uom_qty': 0},
        ])

    def test_kit_return_and_decrease_sol_qty_to_zero(self):
        """
        Create and confirm a SO with a kit product.
        Deliver in two steps & Return the components
        Set the SOL qty to 0

        Check that the move chain is adapted accordingly.
        """
        stock_location = self.company_data['default_warehouse'].lot_stock_id
        self.company_data['default_warehouse'].delivery_steps = 'pick_ship'

        grp_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'group_ids': [(4, grp_uom.id)]})

        # 10 kit_3 = 10 x compo_f + 20 x compo_g
        self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 10)
        self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 20)

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner_a
        with so_form.order_line.new() as line:
            line.product_id = self.kit_3
            line.product_uom_qty = 2
            line.product_uom_id = self.uom_ten
        so = so_form.save()
        so.action_confirm()

        pick = so.picking_ids
        for m in pick.move_ids:
            m.write({'quantity': m.product_uom_qty, 'picked': True})
        pick.button_validate()
        self.assertEqual(pick.state, 'done')
        delivery = so.picking_ids - pick
        for m in delivery.move_ids:
            m.write({'quantity': m.product_uom_qty, 'picked': True})
        delivery.button_validate()
        self.assertEqual(delivery.state, 'done')
        self.assertEqual(so.order_line.qty_delivered, 2)

        ctx = {'active_id': delivery.id, 'active_model': 'stock.picking'}
        return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
        for line in return_wizard.product_return_moves:
            line.quantity = line.move_id.quantity
        return_picking = return_wizard._create_return()
        for m in return_picking.move_ids:
            m.write({'quantity': m.product_uom_qty, 'picked': True})
        return_picking.button_validate()

        self.assertEqual(return_picking.state, 'done')
        self.assertEqual(so.order_line.qty_delivered, 0)

        with Form(so) as so_form:
            with so_form.order_line.edit(0) as line:
                line.product_uom_qty = 0

        self.assertEqual(so.picking_ids, pick | delivery | return_picking)
        self.assertRecordValues(so.picking_ids.move_ids.sorted(lambda m: (m.picking_id.id, m.product_id.id)), [
            {'picking_id': pick.id, 'product_id': self.component_f.id, 'quantity': 20.0},
            {'picking_id': pick.id, 'product_id': self.component_g.id, 'quantity': 40.0},
            {'picking_id': delivery.id, 'product_id': self.component_f.id, 'quantity': 20.0},
            {'picking_id': delivery.id, 'product_id': self.component_g.id, 'quantity': 40.0},
            {'picking_id': return_picking.id, 'product_id': self.component_f.id, 'quantity': 20.0},
            {'picking_id': return_picking.id, 'product_id': self.component_g.id, 'quantity': 40.0},
        ])

    @skip('Temporary to fast merge new valuation')
    def test_fifo_reverse_and_create_new_invoice(self):
        """
        FIFO automated
        Kit with one component
        Receive the component: 1@10, 1@50
        Deliver 1 kit
        Post the invoice, add a credit note with option 'new draft inv'
        Post the second invoice
        COGS should be based on the delivered kit
        """
        kit = self._cls_create_product('Simple Kit', self.uom_unit)
        categ_form = Form(self.env['product.category'])
        categ_form.name = 'Super Fifo'
        categ_form.property_cost_method = 'fifo'
        categ_form.property_valuation = 'real_time'
        categ = categ_form.save()
        (kit + self.component_a).categ_id = categ

        self.env['mrp.bom'].create({
            'product_tmpl_id': kit.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})]
        })

        in_moves = self.env['stock.move'].create([{
            'product_id': self.component_a.id,
            'location_id': self.env.ref('stock.stock_location_suppliers').id,
            'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
            'product_uom': self.component_a.uom_id.id,
            'product_uom_qty': 1,
            'price_unit': p,
        } for p in [10, 50]])
        in_moves._action_confirm()
        in_moves.write({'quantity': 1, 'picked': True})
        in_moves._action_done()

        so = self.env['sale.order'].create({
            'partner_id': self.env['res.partner'].create({'name': 'Test Partner'}).id,
            'order_line': [
                (0, 0, {
                    'name': kit.name,
                    'product_id': kit.id,
                    'product_uom_qty': 1.0,
                    'price_unit': 100,
                    'tax_ids': False,
                })],
        })
        so.action_confirm()

        picking = so.picking_ids
        picking.move_ids.write({'quantity': 1.0, 'picked': True})
        picking.button_validate()

        invoice01 = so._create_invoices()
        invoice01.action_post()

        move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
            'journal_id': invoice01.journal_id.id,
        })
        reversal = move_reversal.modify_moves()
        invoice02 = self.env['account.move'].browse(reversal['res_id'])
        invoice02.action_post()

        amls = invoice02.line_ids
        stock_out_aml = amls.filtered(lambda aml: aml.account_id == categ.property_stock_account_output_categ_id)
        self.assertEqual(stock_out_aml.debit, 0)
        self.assertEqual(stock_out_aml.credit, 10)
        cogs_aml = amls.filtered(lambda aml: aml.account_id == categ.property_account_expense_categ_id)
        self.assertEqual(cogs_aml.debit, 10)
        self.assertEqual(cogs_aml.credit, 0)

    @skip('Temporary to fast merge new valuation')
    def test_kit_avco_amls_reconciliation(self):
        self.stock_account_product_categ.property_cost_method = 'average'

        compo01, compo02, kit = self.env['product.product'].create([{
            'name': name,
            'is_storable': True,
            'standard_price': price,
            'categ_id': self.stock_account_product_categ.id,
            'invoice_policy': 'delivery',
        } for name, price in [
            ('Compo 01', 10),
            ('Compo 02', 20),
            ('Kit', 0),
        ]])

        self.env['stock.quant']._update_available_quantity(compo01, self.company_data['default_warehouse'].lot_stock_id, 1)
        self.env['stock.quant']._update_available_quantity(compo02, self.company_data['default_warehouse'].lot_stock_id, 1)

        self.env['mrp.bom'].create({
            'product_id': kit.id,
            'product_tmpl_id': kit.product_tmpl_id.id,
            'product_uom_id': kit.uom_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'bom_line_ids': [
                (0, 0, {'product_id': compo01.id, 'product_qty': 1.0}),
                (0, 0, {'product_id': compo02.id, 'product_qty': 1.0}),
            ],
        })

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {
                    'name': kit.name,
                    'product_id': kit.id,
                    'product_uom_qty': 1.0,
                    'price_unit': 5,
                    'tax_ids': False,
                })],
        })
        so.action_confirm()
        so.picking_ids.move_line_ids.quantity = 1
        so.picking_ids.move_ids.picked = True
        so.picking_ids.button_validate()

        invoice = so._create_invoices()
        invoice.action_post()

        self.assertEqual(len(invoice.line_ids.filtered('reconciled')), 1)

    def test_avoid_removing_kit_bom_in_use(self):
        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {
                    'name': self.kit_1.name,
                    'product_id': self.kit_1.id,
                    'product_uom_qty': 1.0,
                    'price_unit': 5,
                    'tax_ids': False,
                })],
        })
        self.bom_kit_1.action_archive()
        self.bom_kit_1.action_unarchive()

        so.action_confirm()
        with self.assertRaises(UserError):
            self.bom_kit_1.write({'type': 'normal'})
        with self.assertRaises(UserError):
            self.bom_kit_1.action_archive()
        with self.assertRaises(UserError):
            self.bom_kit_1.unlink()

        for move in so.order_line.move_ids:
            move.write({'quantity': move.product_uom_qty, 'picked': True})
        so.picking_ids.button_validate()

        self.assertEqual(so.picking_ids.state, 'done')
        with self.assertRaises(UserError):
            self.bom_kit_1.write({'type': 'normal'})
        with self.assertRaises(UserError):
            self.bom_kit_1.action_archive()
        with self.assertRaises(UserError):
            self.bom_kit_1.unlink()

        invoice = so._create_invoices()
        invoice.action_post()

        self.assertEqual(invoice.state, 'posted')
        self.bom_kit_1.action_archive()
        self.bom_kit_1.action_unarchive()
        self.bom_kit_1.write({'type': 'normal'})
        self.bom_kit_1.write({'type': 'phantom'})
        self.bom_kit_1.unlink()

    def test_merge_move_kit_on_adding_new_sol(self):
        """
        Create and confirm an SO for 2 similar kit products.
        Add a new sale order line for an other unrelated prodcut.

        Check that the delivery kit moves were not merged by the confirmation of the new move.
        """
        warehouse = self.company_data['default_warehouse']
        warehouse.delivery_steps = 'pick_ship'
        kit = self.kit_3
        # create a similar kit
        bom_copy = kit.bom_ids[0].copy()
        kit_copy = kit.copy()
        bom_copy.product_tmpl_id = kit_copy.product_tmpl_id
        # put component in stock: 10 kit = 10 x comp_f + 20 x comp_g
        self.env['stock.quant']._update_available_quantity(self.component_f, warehouse.lot_stock_id, 10)
        self.env['stock.quant']._update_available_quantity(self.component_g, warehouse.lot_stock_id, 20)
        self.env['stock.quant']._update_available_quantity(self.component_a, warehouse.lot_stock_id, 5)

        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner_a
        with so_form.order_line.new() as line:
            line.product_id = kit
            line.product_uom_qty = 2
        with so_form.order_line.new() as line:
            line.product_id = kit_copy
            line.product_uom_qty = 3
        so = so_form.save()
        so.action_confirm()

        pick = so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id)
        expected_pick_moves = [
            { 'quantity': 2.0, 'product_id': self.component_f.id, 'bom_line_id': kit.bom_ids[0].bom_line_ids.filtered(lambda bl: bl.product_id == self.component_f).id},
            { 'quantity': 3.0, 'product_id': self.component_f.id, 'bom_line_id': bom_copy.bom_line_ids.filtered(lambda bl: bl.product_id == self.component_f).id},
            { 'quantity': 4.0, 'product_id': self.component_g.id, 'bom_line_id': kit.bom_ids[0].bom_line_ids.filtered(lambda bl: bl.product_id == self.component_g).id},
            { 'quantity': 6.0, 'product_id': self.component_g.id, 'bom_line_id': bom_copy.bom_line_ids.filtered(lambda bl: bl.product_id == self.component_g).id},
        ]
        self.assertRecordValues(pick.move_ids.sorted(lambda m: m.quantity), expected_pick_moves)
        with Form(so) as so_form:
            with so_form.order_line.new() as line:
                line.product_id = self.component_a
                line.product_uom_qty = 1
        expected_pick_moves = [
            { 'quantity': 1.0, 'product_id': self.component_a.id, 'bom_line_id': False},
        ] + expected_pick_moves
        self.assertRecordValues(pick.move_ids.sorted(lambda m: m.quantity), expected_pick_moves)

    def test_return_kit_in_quarantine_location(self):
        """
        Return a kit to WH/Return Location
        Push Rule: WH/Return -> WH/Stock
        Ensure the delivered qty is correctly updated
        """
        wh = self.company_data['default_warehouse']
        stock_location = wh.lot_stock_id

        return_location = self.env['stock.location'].create({
            'location_id': stock_location.location_id.id,
            'name': 'Return Location',
            'usage': 'internal',
        })

        self.env['stock.route'].create({
            'name': 'Return Route',
            'warehouse_selectable': True,
            'warehouse_ids': [(4, wh.id)],
            'rule_ids': [(0, 0, {
                'name': 'Return to Stock',
                'location_src_id': return_location.id,
                'location_dest_id': stock_location.id,
                'company_id': self.company_data['company'].id,
                'action': 'push',
                'auto': 'manual',
                'picking_type_id': wh.int_type_id.id,
            })],
        })

        order = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'product_id': self.kit_1.id}),
            ],
        })
        order.action_confirm()

        delivery = order.picking_ids
        for move in delivery.move_ids:
            move.quantity = move.product_qty
        delivery.button_validate()
        self.assertEqual(delivery.state, 'done')

        return_wizard = self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking').create({})
        for line in return_wizard.product_return_moves:
            line.quantity = line.move_quantity
        res = return_wizard.action_create_returns()

        return_picking = self.env['stock.picking'].browse(res["res_id"])
        return_picking.location_dest_id = return_location
        for move in return_picking.move_ids:
            move.quantity = move.product_qty
        return_picking.button_validate()
        self.assertEqual(return_picking.state, 'done')
        self.assertEqual(order.order_line.qty_delivered, 0)

        internal_picking = return_picking.move_ids.move_dest_ids.picking_id
        self.assertTrue(internal_picking)

        for move in internal_picking.move_ids:
            move.quantity = move.product_qty
        internal_picking.button_validate()
        self.assertEqual(internal_picking.state, 'done')
        self.assertEqual(order.order_line.qty_delivered, 0)

    def test_return_for_exchange_kit_product_component(self):
        """ Returning for exchange a kit's component should leave the original sale order line's
        qty_delivered with the correct value.
        """
        for comp in self.bom_kit_1.bom_line_ids.product_id:
            self.env['stock.quant']._update_available_quantity(comp, self.company_data['default_warehouse'].lot_stock_id, quantity=10)

        comp_to_return = self.bom_kit_1.bom_line_ids.filtered(lambda bl: bl.product_qty == 1).product_id
        kit_product = self.kit_1
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [Command.create({
                'product_id': kit_product.id,
                'product_uom_qty': 1.0,
            })],
        })
        sale_order.action_confirm()
        delivery = sale_order.picking_ids
        delivery.action_assign()
        delivery.button_validate()
        return_picking_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking'))
        return_wizard = return_picking_form.save()
        return_wizard.product_return_moves.filtered(lambda prm: prm.product_id == comp_to_return).quantity = 1
        res = return_wizard.action_create_exchanges()
        return_picking = self.env['stock.picking'].browse(res['res_id'])
        return_picking.button_validate()
        exchange_picking = sale_order.picking_ids.filtered(lambda so: so.state != 'done')
        exchange_picking.button_validate()
        self.assertEqual(sale_order.order_line.qty_delivered, 1)

    def test_bidirectional_so_mo_link_with_mtso(self):
        """Test the link from the Manufacturing Order to the Sale Order
        when using the MTSO (Make To Stock or Make To Order) procurement method."""
        # Set the MTO and Manufacture routes on the product
        route_manufacture = self.company_data['default_warehouse'].manufacture_pull_id.route_id
        route_mto = self.company_data['default_warehouse'].mto_pull_id.route_id
        self.product_a.route_ids = [Command.set([route_manufacture.id, route_mto.id])]
        # Set the procure method to 'mts_else_mto'
        route_mto.rule_ids.filtered(lambda r: r.location_dest_id.usage == 'production').procure_method = 'mts_else_mto'
        # Create and confirm a Sale Order
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [Command.create({
                'product_id': self.product_a.id,
                'product_uom_qty': 1.0,
            })],
        })
        sale_order.action_confirm()
        # Check the link between the SO and the MO
        self.assertEqual(sale_order.mrp_production_count, 1)
        mo = sale_order.mrp_production_ids
        self.assertEqual(mo.sale_order_count, 1)

    def test_so_with_kit_and_multiple_same_component(self):
        """Test that a Sale Order with a kit product containing multiple identical components
        can be confirmed, and that the picking is created correctly. Then verify that the
        Sale Order can be cancelled and re-confirmed, resulting in a new picking with moves
        properly linked to each BOM line. Finally, test returning a kit component for exchange."""
        # Create a kit product with two identical components by duplicating the first BOM line
        self.bom_kit_1.bom_line_ids = self.bom_kit_1.bom_line_ids[0]
        self.env['mrp.bom.line'].create([
            {'product_id': self.bom_kit_1.bom_line_ids[0].product_id.id, 'product_qty': 1.0, 'bom_id': self.bom_kit_1.id},
        ])
        # Create a Sale Order with the kit product
        so = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [Command.create({
                'product_id': self.kit_1.id,
                'product_uom_qty': 1.0,
            })],
        })
        so.action_confirm()

        # Check that the picking has 2 moves for the same component
        picking = so.picking_ids
        self.assertEqual(len(picking.move_ids), 2, "There should be 2 moves for the same component in the picking")
        self.assertEqual(picking.move_ids.product_id, self.component_a, "All moves should be for the same component")
        self.assertEqual(picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids, "Each move should be linked to a BOM line of the kit")

        # Cancel the Sale Order
        so._action_cancel()
        self.assertEqual(so.state, 'cancel', "The Sale Order should be cancelled")
        self.assertEqual(picking.state, 'cancel', "The picking should be cancelled when the Sale Order is cancelled")

        # Set the Sale Order back to draft and confirm again
        so.action_draft()
        so.action_confirm()

        # Check that a new picking is created with correct moves
        second_picking = so.picking_ids - picking
        self.assertEqual(len(second_picking.move_ids), 2, "The second picking should have 2 moves for the component")
        self.assertEqual(second_picking.move_ids.product_id, self.component_a, "All moves in the second picking should be for the same component")
        self.assertEqual(second_picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids, "Each move in the second picking should be linked to a BOM line of the kit")
        # Returning for exchange a kit's component
        self.env['stock.quant']._update_available_quantity(self.component_a, self.company_data['default_warehouse'].lot_stock_id, quantity=10)
        second_picking.action_assign()
        second_picking.button_validate()
        return_picking_form = Form(self.env['stock.return.picking'].with_context(active_id=second_picking.id, active_model='stock.picking'))
        return_wizard = return_picking_form.save()
        return_wizard.product_return_moves.filtered(lambda prm: prm.product_id == self.component_a).quantity = 1
        res = return_wizard.action_create_exchanges()
        return_picking = self.env['stock.picking'].browse(res['res_id'])
        return_picking.button_validate()
        exchange_picking = so.picking_ids.filtered(lambda so: so.state == 'assigned')
        exchange_picking.button_validate()
        so.order_line._compute_qty_delivered()
        # In the case where the kit has multiple identical components, only the first BOM line
        # is linked to all moves (this is a known limitation).
        self.assertEqual(exchange_picking.move_ids.bom_line_id, self.bom_kit_1.bom_line_ids[0], "All moves in the exchange picking should be linked to the first BOM line.")
        self.assertEqual(exchange_picking.move_ids.quantity, 2)

    def test_delivery_after_splitting_production(self):
        """
        Test that processing the different MOs of a split production correctly
        updates the picking SM's quantity.
        """
        # Set product up with MTO + Manufacture with (empty) BoM
        product = self._cls_create_product('Split Product', self.uom_unit, routes=[
            self.company_data['default_warehouse'].mto_pull_id.route_id,
            self.company_data['default_warehouse'].manufacture_pull_id.route_id,
        ])
        self.env['mrp.bom'].create({
            'product_tmpl_id': product.product_tmpl_id.id,
            'product_uom_id': self.env.ref('uom.product_uom_unit').id,
        })

        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [Command.create({
                'name': f"2 of {self.product.name}",
                'product_id': product.id,
                'product_uom_qty': 2,
            })],
        })
        sale_order.action_confirm()
        sale_picking = sale_order.picking_ids
        self.assertTrue(sale_picking)

        mo = self.env['mrp.production'].search([('product_id', '=', product.id)], limit=1)
        action = mo.action_split()
        wizard = Form(self.env[action['res_model']].with_context(action['context']))
        wizard.max_batch_size = 1
        wizard.save().action_split()
        self.assertEqual(len(mo.production_group_id.production_ids), 2)

        mo.production_group_id.production_ids[0].button_mark_done()
        self.assertEqual(sale_picking.move_ids.quantity, 1)
        mo.production_group_id.production_ids[1].button_mark_done()
        self.assertEqual(sale_picking.move_ids.quantity, 2)
        sale_picking.button_validate()
        self.assertEqual(sale_order.order_line.qty_delivered, 2.0)

    def test_separate_child_mo_for_shared_component(self):
        """Ensure that when confirming a Sale Order with multiple MTO products
        sharing the same component (which has its own BOM), each parent
        manufacturing order generates its own dedicated child MO instead of
        reusing or updating an existing one.

        This verifies that child MOs are created per parent MO (based on the
        parent production group), so that each manufactured product is tracked
        independently.
        """
        route_mto = self.env.ref('stock.route_warehouse0_mto').id
        (self.product_a | self.product_b | self.product).route_ids = [route_mto]
        self.env["mrp.bom"].create([
            {
                "product_tmpl_id": self.product_a.product_tmpl_id.id,
                "bom_line_ids": [(0, 0, {"product_id": self.product.id, "product_qty": 1.0})],
            },
            {
                "product_tmpl_id": self.product_b.product_tmpl_id.id,
                "bom_line_ids": [(0, 0, {"product_id": self.product.id, "product_qty": 1.0})],
            },
            {
                "product_tmpl_id": self.product.product_tmpl_id.id,
                "bom_line_ids": [(0, 0, {"product_id": self.component_a.id, "product_qty": 1.0})],
            },
        ])
        so = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product_a.id,
                    'product_uom_qty': 1,
                }),
                Command.create({
                    'product_id': self.product_b.id,
                    'product_uom_qty': 1,
                }),
            ],
        })
        so.action_confirm()
        self.assertEqual(so.mrp_production_count, 2)
        self.assertEqual(so.mrp_production_ids[0].mrp_production_child_count, 1)
        self.assertEqual(so.mrp_production_ids[1].mrp_production_child_count, 1)

    def test_sale_mto_manufacture_quantity_update_propagation(self):
        """
        Check that in MTO the quantity update on an SO is propagated on the MO
        and that an activity is scheduled on operation cancellation.
        """
        product = self.product
        product.route_ids = self.env.ref('stock.route_warehouse0_mto')
        self.env['mrp.bom'].create({
            'product_tmpl_id': product.product_tmpl_id.id,
            'bom_line_ids': [Command.create({
                'product_id': self.component_a.id, 'product_qty': 1.0,
            })],
        })
        sale_order, sale_order_to_cancel = sale_orders = self.env['sale.order'].create([{
            'partner_id': self.partner.id,
            'order_line': [Command.create({
                'product_id': product.id,
                'product_uom_qty': 3,
            })],
        } for _ in range(2)])
        sale_orders.action_confirm()

        production = sale_order.stock_reference_ids.production_ids
        self.assertRecordValues(production, [
            {'product_qty': 3.0, 'product_uom_id': product.uom_id.id}
        ])
        # Cancel the delivery which adds a warning in the chatter but does not cancel the MO
        delivery = sale_order.picking_ids
        delivery.action_cancel()
        # Check that an activity was linked on the the MO
        self.assertRecordValues(production.activity_ids, [
            {'user_id': self.env.user.id, 'display_name': 'Exception'}
        ])
        self.assertRegex(production.activity_ids.note, fr"Exception\(s\) occurred on the picking.*\n.*{delivery.name.replace('/', '.')}.*\n.*Manual actions may be needed")
        # Update the selling demand to 10 units, this should create a delivery for
        # 10 units and MTO should adapt existing MO for an additinal 10 units
        with Form(sale_order) as so_form:
            with so_form.order_line.edit(0) as order_line:
                order_line.product_uom_qty = 10.0
        self.assertRecordValues(sale_order.stock_reference_ids.production_ids, [
            {'product_qty': 13.0, 'product_uom_id': product.uom_id.id}
        ])

        # Check that cancelling the SO, propagates the cancellation on the delivery
        # and scheduled a signle activity on the MO (the one of the SO, not the DO)
        sale_order_to_cancel.action_cancel()
        self.assertEqual(sale_order_to_cancel.picking_ids.state, 'cancel')
        production_2 = sale_order_to_cancel.stock_reference_ids.production_ids
        self.assertRecordValues(production_2.activity_ids, [
            {'user_id': self.env.user.id, 'display_name': 'Exception'}
        ])
        self.assertRegex(production_2.activity_ids.note, fr"Exception\(s\) occurred on the sale order\(s\).*\n.*{sale_order_to_cancel.name}.*\n.*Manual actions may be needed")
