# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from freezegun import freeze_time

from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import Form
from odoo.tests.common import TransactionCase
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon

from odoo.tests import tagged
from dateutil.relativedelta import relativedelta


@tagged('post_install', '-at_install')
class TestSubcontractingBasic(TransactionCase):
    def test_subcontracting_location_1(self):
        """ Checks the creation and presence of the subcontracting location. """
        self.assertTrue(self.env.company.subcontracting_location_id)
        self.assertTrue(self.env.company.subcontracting_location_id.active)
        company2 = self.env['res.company'].create({'name': 'Test Company'})
        self.assertTrue(company2.subcontracting_location_id)
        self.assertTrue(self.env.company.subcontracting_location_id != company2.subcontracting_location_id)

    def test_duplicating_warehouses_recreates_their_routes_and_operation_types(self):
        """ Duplicating a warehouse should result in the creation of new routes and operation types.
        Not reusing the existing routes and operation types"""
        wh_original = self.env['stock.warehouse'].search([], limit=1)
        wh_copy = wh_original.copy(default={'name': 'Dummy Warehouse (copy)', 'code': 'Dummy'})

        # Check if warehouse routes got RECREATED (instead of reused)
        route_types = [
            "route_ids",
            "pbm_route_id",
            "subcontracting_route_id",
            "crossdock_route_id",
            "reception_route_id",
            "delivery_route_id"
        ]
        for route_type in route_types:
            original_route_set = wh_original[route_type]
            copy_route_set = wh_copy[route_type]
            error_message = f"At least one {route_type} (route) got reused on duplication (should have been recreated)"
            self.assertEqual(len(original_route_set & copy_route_set), 0, error_message)

        # Check if warehouse operation types (picking.type) got RECREATED (instead of reused)
        operation_types = [
            "subcontracting_type_id",
            "subcontracting_resupply_type_id",
            "pick_type_id",
            "pack_type_id",
            "out_type_id",
            "in_type_id",
            "qc_type_id",
            "store_type_id",
            "int_type_id"
        ]
        for operation_type in operation_types:
            original_type_set = wh_original[operation_type]
            copy_type_set = wh_copy[operation_type]
            error_message = f"At least one {operation_type} (operation_type) got reused on duplication (should have been recreated)"
            self.assertEqual(len(original_type_set & copy_type_set), 0, error_message)

    def test_warehouse_subcontracting_resupply_type_code(self):
        """ Assert that default operation code of resupply subcontractors is 'internal'. """
        warehouse = self.env['stock.warehouse'].create({
            'name': 'Warehouse',
            'code': 'MYWH'
        })
        self.assertEqual(warehouse.subcontracting_resupply_type_id.code, 'internal')

@tagged('post_install', '-at_install')
class TestSubcontractingFlows(TestMrpSubcontractingCommon):
    def test_flow_1(self):
        """ Don't tick any route on the components and trigger the creation of the subcontracting
        manufacturing order through a receipt picking. Create a reordering rule in the
        subcontracting locations for a component and run the scheduler to resupply. Checks if the
        resupplying actually works
        """
        # Check subcontracting picking Type
        self.assertTrue(all(self.env['stock.warehouse'].search([]).with_context(active_test=False).mapped('subcontracting_type_id.use_create_components_lots')))
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # Nothing should be tracked
        self.assertTrue(all(m.product_uom_qty == m.quantity for m in picking_receipt.move_ids))
        self.assertEqual(picking_receipt.state, 'assigned')
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        # Check the created manufacturing order
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
        self.assertEqual(len(mo), 1)
        self.assertEqual(len(mo.picking_ids), 0)
        wh = picking_receipt.picking_type_id.warehouse_id
        self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mo.picking_type_id.active)

        # Create a RR
        pg1 = self.env['procurement.group'].create({})
        self.env['stock.warehouse.orderpoint'].create({
            'name': 'xxx',
            'product_id': self.comp1.id,
            'product_min_qty': 0,
            'product_max_qty': 0,
            'location_id': self.env.user.company_id.subcontracting_location_id.id,
            'group_id': pg1.id,
        })

        # Run the scheduler and check the created picking
        self.env['procurement.group'].run_scheduler()
        picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
        self.assertEqual(len(picking), 1)
        self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
        picking_receipt.move_ids.quantity = 1
        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -1)
        self.assertEqual(avail_qty_comp2, -1)
        self.assertEqual(avail_qty_finished, 1)

        # Ensure returns to subcontractor location
        return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking_receipt.id, active_model='stock.picking'))
        return_wizard = return_form.save()
        return_wizard.product_return_moves.quantity = 1
        return_picking = return_wizard._create_return()
        self.assertEqual(len(return_picking), 1)
        self.assertEqual(return_picking.move_ids.location_dest_id, self.subcontractor_partner1.property_stock_subcontractor)

    def test_flow_2(self):
        """ Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
        the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
        actually works. Also set a different subcontracting location on the partner.
        """
        # Tick "resupply subconractor on order"
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
        # Create a different subcontract location & check rules replication
        reference_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
        partner_subcontract_location = self.env['stock.location'].create({
            'name': 'Specific partner location',
            'location_id': self.env.ref('stock.stock_location_locations_partner').id,
            'usage': 'internal',
            'company_id': self.env.company.id,
            'is_subcontracting_location': True,
        })
        custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', partner_subcontract_location.id), ('location_dest_id', '=', partner_subcontract_location.id)])
        self.assertEqual(reference_location_rules_count, custom_location_rules_count)
        self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id
        # Add a manufacturing lead time to check that the resupply delivery is correctly planned 2 days
        # before the subcontracting receipt
        self.bom.produce_delay = 2

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt = picking_form.save()

        # Nothing should be tracked
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        # Pickings should directly be created
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
        self.assertEqual(len(mo.picking_ids), 1)
        self.assertEqual(mo.state, 'confirmed')
        self.assertEqual(len(mo.picking_ids.move_ids), 2)

        picking = mo.picking_ids
        wh = picking.picking_type_id.warehouse_id

        # The picking should be a delivery order
        self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)
        # The date planned should be correct
        self.assertEqual(picking_receipt.scheduled_date, picking.scheduled_date + relativedelta(days=mo.bom_id.produce_delay))

        self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mo.picking_type_id.active)

        # No manufacturing order for `self.comp2`
        comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
        self.assertEqual(len(comp2mo), 0)

        picking_receipt.move_ids.quantity = 1
        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -1)
        self.assertEqual(avail_qty_comp2, -1)
        self.assertEqual(avail_qty_finished, 1)

        avail_qty_comp1_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp1, self.env.company.subcontracting_location_id, allow_negative=True)
        avail_qty_comp2_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp2, self.env.company.subcontracting_location_id, allow_negative=True)
        self.assertEqual(avail_qty_comp1_in_global_location, 0.0)
        self.assertEqual(avail_qty_comp2_in_global_location, 0.0)

    def test_flow_3(self):
        """ Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
        creation of the subcontracting manufacturing order through a receipt picking. Checks if the
        resupplying actually works. One of the component has also "manufacture" set and a BOM
        linked. Checks that an MO is created for this one.
        """
        # Tick "resupply subconractor on order"
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        (self.comp1 + self.comp2).write({'route_ids': [(6, None, [resupply_sub_on_order_route.id])]})

        # Tick "manufacture" and MTO on self.comp2
        mto_route = self.env.ref('stock.route_warehouse0_mto')
        mto_route.active = True
        manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
        self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
        self.comp2.write({'route_ids': [(4, mto_route.id, None)]})

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # Nothing should be tracked
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        # Pickings should directly be created
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
        self.assertEqual(mo.state, 'confirmed')

        picking_delivery = mo.picking_ids
        self.assertEqual(len(picking_delivery), 1)
        self.assertEqual(len(picking_delivery.move_ids), 2)
        self.assertEqual(picking_delivery.origin, picking_receipt.name)
        self.assertEqual(picking_delivery.partner_id, picking_receipt.partner_id)

        # The picking should be a delivery order
        wh = picking_receipt.picking_type_id.warehouse_id
        self.assertEqual(mo.picking_ids.picking_type_id, wh.subcontracting_resupply_type_id)

        self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mo.picking_type_id.active)

        # As well as a manufacturing order for `self.comp2`
        comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
        self.assertEqual(len(comp2mo), 1)
        picking_receipt.move_ids.quantity = 1
        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -1)
        self.assertEqual(avail_qty_comp2, -1)
        self.assertEqual(avail_qty_finished, 1)

    def test_flow_4(self):
        """ Tick "Manufacture" and "MTO" on the components and trigger the
        creation of the subcontracting manufacturing order through a receipt
        picking. Checks that the delivery and MO for its components are
        automatically created.
        """
        # Required for `location_id` to be visible in the view
        self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
        # Tick "manufacture" and MTO on self.comp2
        mto_route = self.env.ref('stock.route_warehouse0_mto')
        mto_route.active = True
        manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
        self.comp2.write({'route_ids': [(6, None, [manufacture_route.id, mto_route.id])]})

        orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
        orderpoint_form.product_id = self.comp2
        orderpoint_form.product_min_qty = 0.0
        orderpoint_form.product_max_qty = 10.0
        orderpoint_form.location_id = self.env.company.subcontracting_location_id
        orderpoint = orderpoint_form.save()

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt = picking_form.save()

        warehouse = picking_receipt.picking_type_id.warehouse_id

        # Pickings should directly be created
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
        self.assertEqual(mo.state, 'confirmed')

        picking_delivery = mo.picking_ids
        self.assertFalse(picking_delivery)

        picking_delivery = self.env['stock.picking'].search([('origin', 'ilike', '%' + picking_receipt.name + '%')])
        self.assertFalse(picking_delivery)

        move = self.env['stock.move'].search([
            ('product_id', '=', self.comp2.id),
            ('location_id', '=', warehouse.lot_stock_id.id),
            ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
        ])
        self.assertTrue(move)
        picking_delivery = move.picking_id
        self.assertTrue(picking_delivery)
        self.assertEqual(move.product_uom_qty, 11.0)

        # As well as a manufacturing order for `self.comp2`
        comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
        self.assertEqual(len(comp2mo), 1)

    def test_flow_5(self):
        """ Check that the correct BoM is chosen accordingly to the partner
        """
        # We create a second partner of type subcontractor
        main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
        subcontractor_partner2 = self.env['res.partner'].create({
            'name': 'subcontractor_partner',
            'parent_id': main_partner_2.id,
            'company_id': self.env.ref('base.main_company').id
        })

        # We create a different BoM for the same product
        comp3 = self.env['product.product'].create({
            'name': 'Component1',
            'is_storable': True,
            'categ_id': self.env.ref('product.product_category_all').id,
        })

        bom_form = Form(self.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.product_tmpl_id = self.finished.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = self.comp1
            bom_line.product_qty = 1
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = comp3
            bom_line.product_qty = 1
        bom2 = bom_form.save()

        # We assign the second BoM to the new partner
        self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
        bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})

        # Create a receipt picking from the subcontractor1
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt1 = picking_form.save()

        # Create a receipt picking from the subcontractor2
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = subcontractor_partner2
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt2 = picking_form.save()

        mo_pick1 = picking_receipt1.move_ids.mapped('move_orig_ids.production_id')
        mo_pick2 = picking_receipt2.move_ids.mapped('move_orig_ids.production_id')
        self.assertEqual(len(mo_pick1), 1)
        self.assertEqual(len(mo_pick2), 1)
        self.assertEqual(mo_pick1.bom_id, self.bom)
        self.assertEqual(mo_pick2.bom_id, bom2)

    def test_flow_6(self):
        """ Extra quantity on the move.
        """
        # We create a second partner of type subcontractor
        main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
        subcontractor_partner2 = self.env['res.partner'].create({
            'name': 'subcontractor_partner',
            'parent_id': main_partner_2.id,
            'company_id': self.env.ref('base.main_company').id,
        })
        self.env.invalidate_all()

        # We create a different BoM for the same product
        comp3 = self.env['product.product'].create({
            'name': 'Component3',
            'is_storable': True,
            'categ_id': self.env.ref('product.product_category_all').id,
        })

        bom_form = Form(self.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.product_tmpl_id = self.finished.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = self.comp1
            bom_line.product_qty = 1
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = comp3
            bom_line.product_qty = 2
        bom2 = bom_form.save()

        # We assign the second BoM to the new partner
        self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
        bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})

        # Create a receipt picking from the subcontractor1
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = subcontractor_partner2
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        picking_receipt.move_ids.quantity = 3.0
        picking_receipt.move_ids.picked = True
        picking_receipt._action_done()
        mo = picking_receipt._get_subcontract_production()
        move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
        move_comp3 = mo.move_raw_ids.filtered(lambda m: m.product_id == comp3)
        self.assertEqual(sum(move_comp1.mapped('product_uom_qty')), 3.0)
        self.assertEqual(sum(move_comp3.mapped('product_uom_qty')), 6.0)
        self.assertEqual(sum(move_comp1.mapped('quantity')), 3.0)
        self.assertEqual(sum(move_comp3.mapped('quantity')), 6.0)
        move_finished = mo.move_finished_ids
        self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
        self.assertEqual(sum(move_finished.mapped('quantity')), 3.0)

    def test_flow_8(self):
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 5
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        picking_receipt.move_ids.quantity = 3
        picking_receipt.move_ids.picked = True
        Form.from_action(self.env, picking_receipt.button_validate()).save().process()

        backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_receipt.id)])
        self.assertTrue(backorder)
        self.assertEqual(backorder.move_ids.product_uom_qty, 2)
        mo_done = backorder.move_ids.move_orig_ids.production_id.filtered(lambda p: p.state == 'done')
        backorder_mo = backorder.move_ids.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
        self.assertTrue(mo_done)
        self.assertEqual(mo_done.qty_produced, 3)
        self.assertEqual(mo_done.product_uom_qty, 3)
        self.assertTrue(backorder_mo)
        self.assertEqual(backorder_mo.product_uom_qty, 2)
        self.assertEqual(backorder_mo.qty_produced, 0)
        backorder.move_ids.quantity = 2
        backorder.move_ids.picked = True
        backorder._action_done()
        self.assertTrue(picking_receipt.move_ids.move_orig_ids[0].production_id.state == 'done')

    def test_flow_9(self):
        """Ensure that cancel the subcontract moves will also delete the
        components need for the subcontractor.
        """
        resupply_sub_on_order_route = self.env['stock.route'].search([
            ('name', '=', 'Resupply Subcontractor on Order')
        ])
        (self.comp1 + self.comp2).write({
            'route_ids': [(4, resupply_sub_on_order_route.id)]
        })

        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 5
            move.quantity = 5
            move.picked = True
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        picking_delivery = self.env['stock.move'].search([
            ('product_id', 'in', (self.comp1 | self.comp2).ids)
        ]).picking_id
        self.assertTrue(picking_delivery)
        self.assertEqual(picking_delivery.state, 'confirmed')
        self.assertEqual(self.comp1.virtual_available, -5)
        self.assertEqual(self.comp2.virtual_available, -5)
        # action_cancel is not call on the picking in order
        # to test behavior from other source than picking (e.g. puchase).
        picking_receipt.move_ids._action_cancel()
        self.assertEqual(picking_delivery.state, 'cancel')
        self.assertEqual(self.comp1.virtual_available, 0.0)
        self.assertEqual(self.comp1.virtual_available, 0.0)

    def test_flow_10(self):
        """Receipts from a children contact of a subcontractor are properly
        handled.
        """
        # Create a children contact
        subcontractor_contact = self.env['res.partner'].create({
            'name': 'Test children subcontractor contact',
            'parent_id': self.subcontractor_partner1.id,
        })
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = subcontractor_contact
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # Check that a manufacturing order is created
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
        self.assertEqual(len(mo), 1)

    def test_flow_flexible_bom_1(self):
        """ Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
        self.bom.consumption = 'flexible'
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 1
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
        action = picking_receipt.action_record_components()
        mo = self.env['mrp.production'].browse(action['res_id'])
        mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
        mo_form.qty_producing = 1
        with mo_form.move_line_raw_ids.edit(0) as ml:
            self.assertEqual(ml.product_id, self.comp1)
            self.assertEqual(ml.quantity, 1)
            ml.quantity = 2
        mo = mo_form.save()
        mo.subcontracting_record_component()
        self.assertEqual(mo.move_raw_ids[0].move_line_ids.quantity, 2)

        # We should not be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        self.assertEqual(avail_qty_comp1, -2)

    def test_flow_warning_bom_1(self):
        """ Record Component for a bom subcontracted with a flexible and flexible + warning consumption """
        self.bom.consumption = 'warning'
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = 2
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
        action = picking_receipt.action_record_components()
        mo = self.env['mrp.production'].browse(action['res_id'])
        mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
        mo_form.qty_producing = 1
        with mo_form.move_line_raw_ids.edit(0) as ml:
            self.assertEqual(ml.product_id, self.comp1)
            self.assertEqual(ml.quantity, 1)
            ml.quantity = 2
        mo = mo_form.save()
        action_warning = mo.subcontracting_record_component()
        warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
        warning = warning.save()
        warning.action_cancel()

        action_warning = mo.subcontracting_record_component()
        warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
        warning = warning.save()
        action = warning.action_confirm()

        self.assertEqual(mo.move_raw_ids[0].move_line_ids.quantity, 2)

        # Record another over-consumption for the remaining components
        mo_2 = self.env['mrp.production'].browse(action['res_id'])
        with Form(mo_2.with_context(**action['context']), view=action['view_id']) as mo_form:
            mo_form.qty_producing = 1
            with mo_form.move_line_raw_ids.edit(0) as ml:
                self.assertEqual(ml.product_id, self.comp1)
                self.assertEqual(ml.quantity, 1)
                ml.quantity = 3
            mo_2 = mo_form.save()

        action_warning_2 = mo_2.subcontracting_record_component()
        self.assertEqual(action_warning_2.get('res_model'), 'mrp.consumption.warning')
        warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning_2['context']))
        warning = warning.save()
        warning.action_confirm()

        self.assertEqual(mo_2.move_raw_ids[0].move_line_ids.quantity, 3)

        # We should not be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        self.assertEqual(avail_qty_comp1, -5)

    def test_backorder_with_subcontracting(self):
        """Test that a subcontracted move is not marked as picked when its quantity is updated.
        """
        self.bom.consumption = 'warning'
        # Create incoming shipment.
        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.warehouse.in_type_id
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = 5
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.comp1
                move.product_uom_qty = 5
            receipt = picking_form.save()
        receipt.action_confirm()

        # Record the over-consumption of a component
        self.assertTrue(receipt._get_subcontract_production())
        action_record = receipt.action_record_components()
        sbc_mo = self.env['mrp.production'].browse(action_record['res_id'])

        self.assertEqual(receipt.move_ids.mapped('picked'), [False, False])
        self.assertEqual(receipt.move_ids.mapped('quantity'), [5.0, 5.0])
        receipt.move_ids[0].quantity = 2
        receipt.move_ids[1].quantity = 4
        self.assertEqual(receipt.move_ids.mapped('picked'), [False, False])
        self.assertEqual(receipt.move_ids.mapped('quantity'), [2.0, 4.0])
        res = receipt.button_validate()
        wizard = Form(self.env[res['res_model']].with_context(res['context'])).save()
        wizard.process()
        backorder = receipt.backorder_ids
        self.assertEqual(receipt.move_ids.mapped('quantity'), [2.0, 4.0])
        self.assertEqual(backorder.move_ids.mapped('quantity'), [3.0, 1.0])
        backorder.button_validate()
        self.assertEqual(backorder.state, 'done')
        self.assertEqual(backorder.move_ids.mapped('quantity'), [3.0, 1.0])
        self.assertEqual(backorder.move_ids.mapped('picked'), [True, True])

    def test_flow_warning_bom_2(self):
        """ For an initial demand of 10 subcontracted products
            - The production of 3 is recorded, with an over-consumption of its components
            After the picking is validated, check that the over-consumption stays as-is.
        """
        self.bom.consumption = 'warning'
        # Create reception picking
        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.warehouse.in_type_id
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = 10
            receipt = picking_form.save()
        receipt.action_confirm()

        # Record the over-consumption of a component
        self.assertTrue(receipt._get_subcontract_production())
        action_record = receipt.action_record_components()
        sbc_mo = self.env['mrp.production'].browse(action_record['res_id'])
        with Form(sbc_mo.with_context(**action_record['context']), view=action_record['view_id']) as mo_form:
            mo_form.qty_producing = 3
            with mo_form.move_line_raw_ids.edit(0) as ml:
                self.assertEqual(ml.product_id, self.comp1)
                self.assertEqual(ml.quantity, 3)
                ml.quantity = 5
            sbc_mo = mo_form.save()
        # Confirm the over-consumption through the warning
        action_warning = sbc_mo.subcontracting_record_component()
        wizard_warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context'])).save()
        wizard_warning.action_confirm()

        self.assertEqual(sbc_mo.move_raw_ids[0].move_line_ids.quantity, 5)

        # Validate the picking without backorders
        Form.from_action(self.env, receipt.button_validate()).save().process_cancel_backorder()

        # Check that the over-consumption is still present
        self.assertEqual(sbc_mo.move_raw_ids[0].move_line_ids.quantity, 5)

    def test_mrp_report_bom_structure_subcontracting(self):
        self.comp2_bom.write({'type': 'subcontract', 'subcontractor_ids': [Command.link(self.subcontractor_partner1.id)]})
        self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.finished.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 10,
        })
        supplier = self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.comp2.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 5,
        })
        self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.comp2.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 1,
            'min_qty': 5,
        })
        self.assertTrue(supplier.is_subcontractor)
        self.comp1.standard_price = 5
        report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=1, searchVariant=False)
        subcontracting_values = report_values['lines']['subcontracting']
        self.assertEqual(subcontracting_values['name'], self.subcontractor_partner1.display_name)
        self.assertEqual(report_values['lines']['bom_cost'], 20)  # 10 For subcontracting + 5 for comp1 + 5 for subcontracting of comp2_bom
        self.assertEqual(subcontracting_values['bom_cost'], 10)
        self.assertEqual(subcontracting_values['prod_cost'], 10)
        self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 5)
        self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)
        report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=3, searchVariant=False)
        subcontracting_values = report_values['lines']['subcontracting']
        self.assertEqual(report_values['lines']['bom_cost'], 60)  # 30 for subcontracting + 15 for comp1 + 15 for subcontracting of comp2_bom
        self.assertEqual(subcontracting_values['bom_cost'], 30)
        self.assertEqual(subcontracting_values['prod_cost'], 30)
        self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 15)
        self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 15)
        report_values = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, searchQty=5, searchVariant=False)
        subcontracting_values = report_values['lines']['subcontracting']
        self.assertEqual(report_values['lines']['bom_cost'], 80)  # 50 for subcontracting + 25 for comp1 + 5 for subcontracting of comp2_bom
        self.assertEqual(subcontracting_values['bom_cost'], 50)
        self.assertEqual(subcontracting_values['prod_cost'], 50)
        self.assertEqual(report_values['lines']['components'][0]['bom_cost'], 25)
        self.assertEqual(report_values['lines']['components'][1]['bom_cost'], 5)

    def test_several_backorders(self):
        def process_picking(picking, qty):
            picking.move_ids.quantity = qty
            picking.move_ids.picked = True
            action = picking.button_validate()
            if isinstance(action, dict):
                Form.from_action(self.env, action).save().process()

        resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        finished, component = self.env['product.product'].create([{
            'name': 'Finished Product',
            'is_storable': True,
        }, {
            'name': 'Component',
            'is_storable': True,
            'route_ids': [(4, resupply_route.id)],
        }])

        bom = self.env['mrp.bom'].create({
            'product_tmpl_id': finished.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'subcontract',
            'subcontractor_ids': [(4, self.subcontractor_partner1.id)],
            'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})],
        })

        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = finished
            move.product_uom_qty = 5
        picking = picking_form.save()
        picking.action_confirm()

        supply_picking = self.env['mrp.production'].search([('bom_id', '=', bom.id)]).picking_ids
        process_picking(supply_picking, 5)

        process_picking(picking, 1.25)

        backorder01 = picking.backorder_ids
        process_picking(backorder01, 1)

        backorder02 = backorder01.backorder_ids
        self.assertEqual(backorder02.move_ids.quantity, 2.75)

        self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)

    def test_several_backorders_2(self):
        # This test ensure that the backorders finished moves are correctly made (Production -> Subcontracting -> Stock)
        # When the receipt is done, the Subcontracting location should have quantity 0 of the finished product.
        # In more detail, this test checks that everything is done correctly
        # when the quantity of the backorder is set on the stock.move.line instead of the stock.move,
        # it can, for example, happen if the finished product is tracked by Serial Number.

        def process_picking_with_backorder(picking, qty):
            # Process the picking by putting the given quantity on the stock.move.line
            move_line = picking.move_line_ids.ensure_one()
            move_line.quantity = qty
            move_line.picked = True
            action = picking.button_validate()
            if isinstance(action, dict):
                Form.from_action(self.env, action).save().process()
            return picking.backorder_ids

        def check_quants(product, stock_qty, sub_qty, prod_qty):
            # Check the quantities of the Stock, Subcontracting and Production locations for the given product
            subcontracting_location = self.env.company.subcontracting_location_id
            production_location = product.property_stock_production
            stock_location = self.env.ref('stock.stock_location_stock')

            self.assertEqual(sub_qty, self.env['stock.quant']._gather(product, subcontracting_location).quantity)
            self.assertEqual(stock_qty, self.env['stock.quant']._gather(product, stock_location).quantity)
            self.assertEqual(prod_qty, self.env['stock.quant']._gather(product, production_location).quantity)

        in_pck_type = self.env.ref('stock.picking_type_in')
        in_pck_type.write({'show_operations': True})

        finished = self.env['product.product'].create({'name': 'Finished Product', 'is_storable': True})
        component = self.env['product.product'].create([{'name': 'Component', 'is_storable': True}])
        self.env['mrp.bom'].create({
            'product_tmpl_id': finished.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'subcontract',
            'subcontractor_ids': [(4, self.subcontractor_partner1.id)],
            'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})],
        })

        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = in_pck_type
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = finished
            move.product_uom_qty = 6
        picking = picking_form.save()
        picking.action_confirm()

        backorder01 = process_picking_with_backorder(picking, 1)
        check_quants(product=finished, stock_qty=1, sub_qty=0, prod_qty=-1)
        check_quants(product=component, stock_qty=0, sub_qty=-1, prod_qty=1)

        backorder02 = process_picking_with_backorder(backorder01, 2)
        check_quants(product=finished, stock_qty=3, sub_qty=0, prod_qty=-3)
        check_quants(product=component, stock_qty=0, sub_qty=-3, prod_qty=3)

        process_picking_with_backorder(backorder02, 3)
        check_quants(product=finished, stock_qty=6, sub_qty=0, prod_qty=-6)
        check_quants(product=component, stock_qty=0, sub_qty=-6, prod_qty=6)

    def test_subcontracting_rules_replication(self):
        """ Test activate/archive subcontracting location rules."""
        reference_location_rules = self.env['stock.rule'].search(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
        warehouse_related_rules = reference_location_rules.filtered(lambda r: r.warehouse_id)
        company_rules = reference_location_rules - warehouse_related_rules
        # Create a custom subcontracting location
        custom_subcontracting_location = self.env['stock.location'].create({
            'name': 'Custom Subcontracting Location',
            'location_id': self.env.ref('stock.stock_location_locations').id,
            'usage': 'internal',
            'company_id': self.env.company.id,
            'is_subcontracting_location': True,
        })
        custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
        self.assertEqual(len(reference_location_rules), custom_location_rules_count)
        # Add a new warehouse
        warehouse = self.env['stock.warehouse'].create({
            'name': 'Additional Warehouse',
            'code': 'ADD'
        })
        company_subcontracting_locations_rules_count = self.env['stock.rule'].search_count(['&', ('company_id', '=', warehouse.company_id.id), '|', ('location_src_id.is_subcontracting_location', '=', True), ('location_dest_id.is_subcontracting_location', '=', True)])
        self.assertEqual(len(warehouse_related_rules) * 4 + len(company_rules) * 2, company_subcontracting_locations_rules_count)
        # Custom location no longer a subcontracting one
        custom_subcontracting_location.is_subcontracting_location = False
        custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
        self.assertEqual(custom_location_rules_count, 0)

    def test_subcontracting_date_warning(self):
        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = 3
                move.quantity = 3
            picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        self.assertEqual(picking_form.json_popover, False)

        subcontract = picking_receipt._get_subcontract_production()
        self.assertEqual(subcontract.date_start, picking_receipt.scheduled_date)
        self.assertEqual(subcontract.date_finished, picking_receipt.scheduled_date)

    def test_subcontracting_set_quantity_done(self):
        """ Tests to set a quantity done directly on a subcontracted move without using the subcontracting wizard.
            Checks that it does the same as it would do with the wizard.
        """
        self.bom.consumption = 'flexible'
        quantities = [10, 15, 12, 14]

        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = quantities[0]
            picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        move = picking_receipt.move_ids_without_package

        for qty in quantities[1:]:
            move.quantity = qty
            subcontracted = move._get_subcontract_production().filtered(lambda p: p.state != 'cancel')
            self.assertEqual(sum(subcontracted.mapped('product_qty')), qty)
            self.assertEqual(move.product_uom_qty, quantities[0])

        picking_receipt.button_validate()
        self.assertEqual(move.product_uom_qty, quantities[0])
        self.assertEqual(move.quantity, quantities[-1])
        subcontracted = move._get_subcontract_production().filtered(lambda p: p.state == 'done')
        self.assertEqual(sum(subcontracted.mapped('qty_produced')), quantities[-1])

    def test_change_reception_serial(self):
        self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('stock.group_production_lot').id)]})
        self.finished.tracking = 'serial'
        self.bom.consumption = 'flexible'

        finished_lots = self.env['stock.lot'].create([{
            'name': 'lot_%s' % number,
            'product_id': self.finished.id,
        } for number in range(3)])

        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = 3
            picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # Register serial number for each finished product
        for lot in finished_lots:
            action = picking_receipt.move_ids.action_show_details()
            self.assertEqual(action['name'], 'Subcontract', "It should open the subcontract record components wizard instead.")
            mo = self.env['mrp.production'].browse(action['res_id'])
            with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
                mo_form.qty_producing = 1
                mo_form.lot_producing_id = lot
                mo_form.save()
            mo.subcontracting_record_component()

        subcontract_move = picking_receipt.move_ids_without_package.filtered(lambda m: m.is_subcontract)
        self.assertEqual(len(subcontract_move._get_subcontract_production()), 3)
        self.assertEqual(len(subcontract_move._get_subcontract_production().lot_producing_id), 3)
        self.assertRecordValues(subcontract_move._get_subcontract_production().lot_producing_id.sorted('id'), [
            {'id': finished_lots[0].id},
            {'id': finished_lots[1].id},
            {'id': finished_lots[2].id},
        ])

        new_lot = self.env['stock.lot'].create({
            'name': 'lot_alter',
            'product_id': self.finished.id,
        })
        action = picking_receipt.move_ids.action_show_details()
        self.assertEqual(action['name'], 'Detailed Operations', "The subcontract record components wizard shouldn't be available now.")
        with Form(subcontract_move.with_context(action['context']), view=action['view_id']) as move_form:
            with move_form.move_line_ids.edit(2) as move_line:
                move_line.lot_id = new_lot
            move_form.save()

        subcontracted_mo = subcontract_move._get_subcontract_production()
        self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_id == new_lot)), 1)
        self.assertEqual(len(subcontracted_mo.filtered(lambda p: p.lot_producing_id != new_lot)), 2)

    def test_multiple_component_records_for_incomplete_move(self):
        self.bom.consumption = 'flexible'
        with Form(self.env['stock.picking']) as picking_form:
            picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
            picking_form.partner_id = self.subcontractor_partner1
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.finished
                move.product_uom_qty = 10
            picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        move = picking_receipt.move_ids_without_package

        # Register the five first finished products
        action = move.action_show_details()
        mo = self.env['mrp.production'].browse(action['res_id'])
        with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
            mo_form.qty_producing = 5
            mo_form.save()
        mo.subcontracting_record_component()
        self.assertEqual(move.quantity, 5)

        # Register two other finished products
        action = move.action_show_details()
        mo = self.env['mrp.production'].browse(action['res_id'])
        with Form(mo.with_context(action['context']), view=action['view_id']) as mo_form:
            mo_form.qty_producing = 2
            mo_form.save()
        mo.subcontracting_record_component()
        self.assertEqual(move.quantity, 7)

        # Validate picking without backorder
        Form.from_action(self.env, picking_receipt.button_validate()).save().process_cancel_backorder()

        self.assertRecordValues(move._get_subcontract_production(), [
            {'product_qty': 5, 'state': 'done'},
            {'product_qty': 2, 'state': 'done'},
            {'product_qty': 3, 'state': 'cancel'},
        ])

    def test_decrease_quantity_done(self):
        self.bom.consumption = 'flexible'
        supplier_location = self.env.ref('stock.stock_location_suppliers')
        uom_duo = self.env['uom.uom'].create({
            'category_id': self.finished.uom_id.category_id.id,
            'name': 'Duos',
            'uom_type': 'bigger',
            'factor_inv': 2.0,
        })

        receipt = self.env['stock.picking'].create({
            'partner_id': self.subcontractor_partner1.id,
            'location_id': supplier_location.id,
            'location_dest_id': self.warehouse.lot_stock_id.id,
            'picking_type_id': self.warehouse.in_type_id.id,
            'move_ids': [(0, 0, {
                'name': self.finished.name,
                'product_id': self.finished.id,
                'product_uom_qty': 10.0,
                'product_uom': uom_duo.id,
                'location_id': supplier_location.id,
                'location_dest_id': self.warehouse.lot_stock_id.id,
            })],
        })

        receipt.action_confirm()
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertRecordValues(productions, [
            {'qty_producing': 0.0, 'product_qty': 10.0, 'state': 'confirmed'},
        ])

        receipt.move_ids.quantity = 6
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
        self.assertRecordValues(productions, [
            {'qty_producing': 6.0, 'product_qty': 6.0, 'state': 'to_close'},
            {'qty_producing': 4.0, 'product_qty': 4.0, 'state': 'to_close'},
        ])

        receipt.move_ids.quantity = 9
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
        self.assertRecordValues(productions, [
            {'qty_producing': 6.0, 'product_qty': 6.0, 'state': 'to_close'},
            {'qty_producing': 3.0, 'product_qty': 3.0, 'state': 'to_close'},
            {'qty_producing': 1.0, 'product_qty': 1.0, 'state': 'to_close'},
        ])

        receipt.move_ids.quantity = 7
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
        self.assertRecordValues(productions, [
            {'qty_producing': 6.0, 'product_qty': 6.0, 'state': 'to_close'},
            {'qty_producing': 1.0, 'product_qty': 1.0, 'state': 'to_close'},
            {'qty_producing': 3.0, 'product_qty': 3.0, 'state': 'to_close'},
        ])

        receipt.move_ids.quantity = 4
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
        self.assertRecordValues(productions, [
            {'qty_producing': 4.0, 'product_qty': 4.0, 'state': 'to_close'},
            {'qty_producing': 1.0, 'product_qty': 1.0, 'state': 'cancel'},
            {'qty_producing': 6.0, 'product_qty': 6.0, 'state': 'to_close'},
        ])

        receipt.move_ids.quantity = 0
        productions = self.env['mrp.production'].search([('product_id', '=', self.finished.id)], order='id')
        self.assertEqual(receipt.move_ids.product_uom_qty, 10.0, 'Demand should not be impacted')
        self.assertRecordValues(productions, [
            {'qty_producing': 4.0, 'product_qty': 4.0, 'state': 'cancel'},
            {'qty_producing': 1.0, 'product_qty': 1.0, 'state': 'cancel'},
            {'qty_producing': 10.0, 'product_qty': 10.0, 'state': 'to_close'},
        ])

    @freeze_time('2024-01-01')
    def test_bom_overview_availability(self):
        # Create routes for components and the main product
        self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.finished.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 1.0,
            'delay': 10
        })
        self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.comp1.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 648.0,
            'delay': 5
        })
        self.env['product.supplierinfo'].create({
            'product_tmpl_id': self.comp2.product_tmpl_id.id,
            'partner_id': self.subcontractor_partner1.id,
            'price': 648.0,
            'delay': 5
        })

        self.bom.produce_delay = 1
        self.bom.days_to_prepare_mo = 3

        # Add 4 units of each component to subcontractor's location
        subcontractor_location = self.env.company.subcontracting_location_id
        self.env['stock.quant']._update_available_quantity(self.comp1, subcontractor_location, 4)
        self.env['stock.quant']._update_available_quantity(self.comp2, subcontractor_location, 4)

        # Generate a report for 3 products: all products should be ready for production
        bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 3)

        self.assertTrue(bom_data['lines']['components_available'])
        for component in bom_data['lines']['components']:
            self.assertEqual(component['quantity_on_hand'], 4)
            self.assertEqual(component['availability_state'], 'available')
        self.assertEqual(bom_data['lines']['earliest_capacity'], 3)
        self.assertEqual(bom_data['lines']['earliest_date'], '01/11/2024')
        self.assertTrue('leftover_capacity' not in bom_data['lines']['earliest_date'])
        self.assertTrue('leftover_date' not in bom_data['lines']['earliest_date'])

        # Generate a report for 5 products: only 4 products should be ready for production
        bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 5)

        self.assertFalse(bom_data['lines']['components_available'])
        for component in bom_data['lines']['components']:
            self.assertEqual(component['quantity_on_hand'], 4)
            self.assertEqual(component['availability_state'], 'estimated')
        self.assertEqual(bom_data['lines']['earliest_capacity'], 4)
        self.assertEqual(bom_data['lines']['earliest_date'], '01/11/2024')
        self.assertEqual(bom_data['lines']['leftover_capacity'], 1)
        self.assertEqual(bom_data['lines']['leftover_date'], '01/16/2024')

    def test_change_partner_subcontracting_location(self):
        """On creating a subcontrating picking, the destination location of the picking is equal to
        the subcontracting location of the contact if specified. Otherwise, it will be equal to the
        default warehouse subcontracting location.
        """
        custom_subcontract_location = self.env['stock.location'].create({
            'name': 'custom partner location',
            'location_id': self.env.ref('stock.stock_location_locations_partner').id,
            'usage': 'internal',
            'company_id': self.env.company.id,
            'is_subcontracting_location': True,
        })
        subcontractor = self.env['res.partner'].create({'name': 'subcontractor'})

        def create_picking(subcontractor):
            picking_form = Form(self.env['stock.picking'])
            picking_form.picking_type_id = self.warehouse.subcontracting_resupply_type_id
            picking_form.partner_id = subcontractor
            with picking_form.move_ids_without_package.new() as move:
                move.product_id = self.comp1
                move.product_uom_qty = 1.0
            picking = picking_form.save()
            picking.action_confirm()
            return picking

        picking_with_default_location = create_picking(subcontractor)
        self.assertEqual(picking_with_default_location.location_dest_id, self.warehouse.subcontracting_resupply_type_id.default_location_dest_id)

        subcontractor.property_stock_subcontractor = custom_subcontract_location.id
        picking_with_custom_location = create_picking(subcontractor)
        self.assertEqual(picking_with_custom_location.location_dest_id, custom_subcontract_location)

@tagged('post_install', '-at_install')
class TestSubcontractingTracking(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})

        # 1: Create a subcontracting partner
        main_company_1 = cls.env['res.partner'].create({'name': 'main_partner'})
        cls.subcontractor_partner1 = cls.env['res.partner'].create({
            'name': 'Subcontractor 1',
            'parent_id': main_company_1.id,
            'company_id': cls.env.ref('base.main_company').id
        })

        # 2. Create a BOM of subcontracting type
        # 2.1. Comp1 has tracking by lot
        cls.comp1_sn = cls.env['product.product'].create({
            'name': 'Component1',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
            'tracking': 'serial'
        })
        cls.comp2 = cls.env['product.product'].create({
            'name': 'Component2',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
        })

        # 2.2. Finished prodcut has tracking by serial number
        cls.finished_product = cls.env['product.product'].create({
            'name': 'finished',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
            'tracking': 'lot'
        })
        bom_form = Form(cls.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.consumption = 'strict'
        bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
        bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = cls.comp1_sn
            bom_line.product_qty = 1
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = cls.comp2
            bom_line.product_qty = 1
        cls.bom_tracked = bom_form.save()

    def test_flow_tracked_1(self):
        """ This test mimics test_flow_1 but with a BoM that has tracking included in it.
        """
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished_product
            move.product_uom_qty = 1
            move.quantity = 1
            move.picked = True
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # We should be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')

        # Check the created manufacturing order
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
        self.assertEqual(len(mo), 1)
        self.assertEqual(len(mo.picking_ids), 0)
        wh = picking_receipt.picking_type_id.warehouse_id
        self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mo.picking_type_id.active)

        # Create a RR
        pg1 = self.env['procurement.group'].create({})
        self.env['stock.warehouse.orderpoint'].create({
            'name': 'xxx',
            'product_id': self.comp1_sn.id,
            'product_min_qty': 0,
            'product_max_qty': 0,
            'location_id': self.env.user.company_id.subcontracting_location_id.id,
            'group_id': pg1.id,
        })

        # Run the scheduler and check the created picking
        self.env['procurement.group'].run_scheduler()
        picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
        self.assertEqual(len(picking), 1)
        self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id)

        lot_id = self.env['stock.lot'].create({
            'name': 'lot1',
            'product_id': self.finished_product.id,
        })
        serial_id = self.env['stock.lot'].create({
            'name': 'lot1',
            'product_id': self.comp1_sn.id,
        })

        action = picking_receipt.action_record_components()
        mo = self.env['mrp.production'].browse(action['res_id'])
        mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
        mo_form.qty_producing = 1
        mo_form.lot_producing_id = lot_id
        with mo_form.move_line_raw_ids.edit(0) as ml:
            ml.lot_id = serial_id
        mo = mo_form.save()
        mo.subcontracting_record_component()

        # We should not be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -1)
        self.assertEqual(avail_qty_comp2, -1)
        self.assertEqual(avail_qty_finished, 1)

    def test_flow_tracked_only_finished(self):
        """ Test when only the finished product is tracked """
        self.finished_product.tracking = "serial"
        self.comp1_sn.tracking = "none"
        nb_finished_product = 3
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished_product
            move.product_uom_qty = nb_finished_product
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        picking_receipt.do_unreserve()

        # We shouldn't be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        wh = picking_receipt.picking_type_id.warehouse_id
        lot_names_finished = [f"subtracked_{i}" for i in range(nb_finished_product)]

        move_details = Form(picking_receipt.move_ids, view='stock.view_stock_move_operations')
        for lot_name in lot_names_finished:
            with move_details.move_line_ids.new() as ml:
                ml.quantity = 1
                ml.lot_name = lot_name
        move_details.save()
        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        # Check the created manufacturing order
        # Should have one mo by serial number
        mos = picking_receipt.move_ids.move_orig_ids.production_id
        self.assertEqual(len(mos), nb_finished_product)
        self.assertEqual(mos.mapped("state"), ["done"] * nb_finished_product)
        self.assertEqual(mos.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mos.picking_type_id.active)
        self.assertEqual(set(mos.lot_producing_id.mapped("name")), set(lot_names_finished))

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -nb_finished_product)
        self.assertEqual(avail_qty_comp2, -nb_finished_product)
        self.assertEqual(avail_qty_finished, nb_finished_product)

    def test_flow_tracked_backorder(self):
        """ This test uses tracked (serial and lot) component and tracked (serial) finished product """
        todo_nb = 4
        self.comp2.tracking = 'lot'
        self.finished_product.tracking = 'serial'

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished_product
            move.product_uom_qty = todo_nb
            move.quantity = todo_nb
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # We should be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'mandatory')

        # Check the created manufacturing order
        mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
        self.assertEqual(len(mo), 1)
        self.assertEqual(len(mo.picking_ids), 0)
        wh = picking_receipt.picking_type_id.warehouse_id
        self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
        self.assertFalse(mo.picking_type_id.active)

        lot_comp2 = self.env['stock.lot'].create({
            'name': 'lot_comp2',
            'product_id': self.comp2.id,
        })
        serials_finished = []
        serials_comp1 = []
        for i in range(todo_nb):
            serials_finished.append(self.env['stock.lot'].create({
                'name': 'serial_fin_%s' % i,
                'product_id': self.finished_product.id,
            }))
            serials_comp1.append(self.env['stock.lot'].create({
                'name': 'serials_comp1_%s' % i,
                'product_id': self.comp1_sn.id,
            }))

        for i in range(todo_nb):
            action = picking_receipt.action_record_components()
            mo = self.env['mrp.production'].browse(action['res_id'])
            mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
            mo_form.lot_producing_id = serials_finished[i]
            with mo_form.move_line_raw_ids.edit(0) as ml:
                self.assertEqual(ml.product_id, self.comp1_sn)
                ml.lot_id = serials_comp1[i]
            with mo_form.move_line_raw_ids.edit(1) as ml:
                self.assertEqual(ml.product_id, self.comp2)
                ml.lot_id = lot_comp2
            mo = mo_form.save()
            mo.subcontracting_record_component()

        # We should not be able to call the 'record_components' button
        self.assertEqual(picking_receipt.display_action_record_components, 'hide')

        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')
        self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done'] * todo_nb)
        self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
        self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"), [1] * todo_nb)

        # Available quantities should be negative at the subcontracting location for each components
        avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
        avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
        self.assertEqual(avail_qty_comp1, -todo_nb)
        self.assertEqual(avail_qty_comp2, -todo_nb)
        self.assertEqual(avail_qty_finished, todo_nb)

    def test_flow_tracked_backorder02(self):
        """ Both component and finished product are tracked by lot. """
        todo_nb = 4
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        finished_product, component = self.env['product.product'].create([{
            'name': 'SuperProduct',
            'is_storable': True,
            'tracking': 'lot',
        }, {
            'name': 'Component',
            'is_storable': True,
            'tracking': 'lot',
            'route_ids': [(4, resupply_sub_on_order_route.id)],
        }])

        bom_form = Form(self.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.subcontractor_ids.add(self.subcontractor_partner1)
        bom_form.product_tmpl_id = finished_product.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = component
            bom_line.product_qty = 1
        bom = bom_form.save()

        finished_lot, component_lot = self.env['stock.lot'].create([{
            'name': 'lot_%s' % product.name,
            'product_id': product.id,
        } for product in [finished_product, component]])

        self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb, lot_id=component_lot)

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = finished_product
            move.product_uom_qty = todo_nb
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])

        # Process the delivery of the components
        compo_picking = mo.picking_ids
        compo_picking.action_assign()
        compo_picking.button_validate()

        for qty in [3, 1]:
            # Record the receiption of <qty> finished products
            picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
            action = picking_receipt.action_record_components()
            mo = self.env['mrp.production'].browse(action['res_id'])
            mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
            mo_form.qty_producing = qty
            mo_form.lot_producing_id = finished_lot
            with mo_form.move_line_raw_ids.edit(0) as ml:
                ml.lot_id = component_lot
            mo = mo_form.save()
            mo.subcontracting_record_component()

            # Validate the picking and create a backorder
            wizard_data = picking_receipt.button_validate()
            if qty == 3:
                Form.from_action(self.env, wizard_data).save().process()

            self.assertEqual(picking_receipt.state, 'done')

    def test_flow_backorder_production(self):
        """ Test subcontracted MO backorder (i.e. through record production window, NOT through
        picking backorder). Finished product is serial tracked to ensure subcontracting MO window
        is opened. Check that MO backorder auto-reserves components
        """
        todo_nb = 3
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        finished_product, component = self.env['product.product'].create([{
            'name': 'Pepper Spray',
            'is_storable': True,
            'tracking': 'serial',
        }, {
            'name': 'Pepper',
            'is_storable': True,
            'route_ids': [(4, resupply_sub_on_order_route.id)],
        }])

        bom_form = Form(self.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.subcontractor_ids.add(self.subcontractor_partner1)
        bom_form.product_tmpl_id = finished_product.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = component
            bom_line.product_qty = 1
        bom = bom_form.save()

        finished_serials = self.env['stock.lot'].create([{
            'name': 'sn_%s' % str(i),
            'product_id': finished_product.id,
        } for i in range(todo_nb)])

        self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb)

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = finished_product
            move.product_uom_qty = todo_nb
            move.quantity = todo_nb
            move.picked = True
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])

        # Process the delivery of the components
        compo_picking = mo.picking_ids
        compo_picking.action_assign()
        compo_picking.button_validate()

        picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
        for sn in finished_serials:
            # Record the production of each serial number separately
            action = picking_receipt.action_record_components()
            mo = self.env['mrp.production'].browse(action['res_id'])
            mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
            mo_form.qty_producing = 1
            mo_form.lot_producing_id = sn
            mo = mo_form.save()
            mo.subcontracting_record_component()

        # Validate the picking
        picking_receipt.button_validate()
        self.assertEqual(picking_receipt.state, 'done')

    def test_flow_mass_produce_tracked_product(self):
        """
        Test the mass production process for subcontracted tracked final products
        """
        todo_nb = 3
        resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        finished_product, component = self.env['product.product'].create([{
            'name': 'Pepper Spray',
            'is_storable': True,
            'tracking': 'serial',
        }, {
            'name': 'Pepper',
            'is_storable': True,
            'route_ids': [(4, resupply_sub_on_order_route.id)],
        }])

        bom_form = Form(self.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.subcontractor_ids.add(self.subcontractor_partner1)
        bom_form.product_tmpl_id = finished_product.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = component
            bom_line.product_qty = 1
        bom = bom_form.save()

        self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb)

        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = finished_product
            move.product_uom_qty = todo_nb
            move.quantity = todo_nb
            move.picked = True
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)], limit=1)
        initial_name = mo.name

        # Process the delivery of the components
        compo_picking = mo.picking_ids
        compo_picking.action_assign()
        compo_picking.button_validate()

        batch_produce_action = mo.button_mark_done()
        wizard = Form(self.env['mrp.batch.produce'].with_context(**batch_produce_action['context']))
        # Let the wizard generate all serial numbers
        wizard.lot_name = "sn#1"
        wizard.lot_qty = todo_nb
        wizard = wizard.save()
        wizard.action_generate_production_text()
        wizard.action_prepare()

        # Each generated serial number should have its own mo
        self.assertRecordValues(mo.procurement_group_id.mrp_production_ids.sorted("name"), [
            {"name": initial_name + "-001", "state": "confirmed"},
            {"name": initial_name + "-002", "state": "confirmed"},
            {"name": initial_name + "-003", "state": "confirmed"},
        ])
        self.assertRecordValues(mo.procurement_group_id.mrp_production_ids.move_raw_ids, [
            {"quantity": 1.0, "state": "assigned"},
            {"quantity": 1.0, "state": "assigned"},
            {"quantity": 1.0, "state": "assigned"},
        ])
        mo.procurement_group_id.mrp_production_ids.button_mark_done()
        self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done', 'done' , 'done'])


@tagged('post_install', '-at_install')
class TestSubcontractingPortal(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('stock.group_production_lot').id)]})
        # 1: Create a subcontracting partner
        main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
        cls.subcontractor_partner1 = cls.env['res.partner'].create({
            'name': 'subcontractor_partner',
            'parent_id': main_partner.id,
            'company_id': cls.env.ref('base.main_company').id,
        })
        # Make the subcontracting partner a portal user
        cls.portal_user = cls.env['res.users'].create({
            'name': 'portal user (subcontractor)',
            'partner_id': cls.subcontractor_partner1.id,
            'login': 'subcontractor',
            'password': 'subcontractor',
            'email': 'subcontractor@subcontracting.portal',
            'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id, cls.env.ref('stock.group_production_lot').id])]
        })

        # 2. Create a BOM of subcontracting type
        # 2.1. Comp1 has tracking by lot
        cls.comp1_sn = cls.env['product.product'].create({
            'name': 'Component1',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
            'tracking': 'serial'
        })
        cls.comp2 = cls.env['product.product'].create({
            'name': 'Component2',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
        })
        cls.product_not_in_bom = cls.env['product.product'].create({
            'name': 'Product not in the BoM',
            'is_storable': True,
        })

        # 2.2. Finished prodcut has tracking by serial number
        cls.finished_product = cls.env['product.product'].create({
            'name': 'finished',
            'is_storable': True,
            'categ_id': cls.env.ref('product.product_category_all').id,
            'tracking': 'lot'
        })
        bom_form = Form(cls.env['mrp.bom'])
        bom_form.type = 'subcontract'
        bom_form.consumption = 'warning'
        bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
        bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = cls.comp1_sn
            bom_line.product_qty = 1
        with bom_form.bom_line_ids.new() as bom_line:
            bom_line.product_id = cls.comp2
            bom_line.product_qty = 1
        cls.bom_tracked = bom_form.save()

    def test_flow_subcontracting_portal(self):
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor_partner1
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished_product
            move.product_uom_qty = 2
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()

        # Using the subcontractor (portal user)
        lot1 = self.env['stock.lot'].with_user(self.portal_user).create({
            'name': 'lot1',
            'product_id': self.finished_product.id,
        })
        lot2 = self.env['stock.lot'].with_user(self.portal_user).create({
            'name': 'lot2',
            'product_id': self.finished_product.id,
        })
        serial1 = self.env['stock.lot'].with_user(self.portal_user).create({
            'name': 'lot1',
            'product_id': self.comp1_sn.id,
        })
        serial2 = self.env['stock.lot'].with_user(self.portal_user).create({
            'name': 'lot2',
            'product_id': self.comp1_sn.id,
        })
        serial3 = self.env['stock.lot'].with_user(self.portal_user).create({
            'name': 'lot3',
            'product_id': self.comp1_sn.id,
        })
        action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
        mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
        mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
        # Registering components for the first manufactured product
        mo_form.qty_producing = 1
        mo_form.lot_producing_id = lot1
        with mo_form.move_line_raw_ids.edit(0) as ml:
            ml.lot_id = serial1
        mo = mo_form.save()
        mo.subcontracting_record_component()
        # Continue record of components with new MO (backorder was when recording first MO)
        action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details()
        mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id'])
        mo_form = Form(mo.with_context(action['context']), view=action['view_id'])
        # Registering components for the second manufactured product with over-consumption, which leads to a warning
        mo_form.qty_producing = 1
        mo_form.lot_producing_id = lot2
        with mo_form.move_line_raw_ids.edit(0) as ml:
            ml.lot_id = serial2
        with mo_form.move_line_raw_ids.new() as ml:
            ml.product_id = self.comp1_sn
            ml.lot_id = serial3
        with mo_form.move_line_raw_ids.edit(1) as ml:
            ml.quantity = 2
        # The portal user should not be able to add a product not in the BoM
        with self.assertRaises(AccessError):
            with mo_form.move_line_raw_ids.new() as ml:
                ml.product_id = self.product_not_in_bom
        mo = mo_form.save()
        action_warning = mo.subcontracting_record_component()
        warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context']))
        warning = warning.save()
        warning.action_confirm()

        # Attempt to validate from the portal user should give an error
        with self.assertRaises(UserError):
            picking_receipt.with_user(self.portal_user).button_validate()

        # Validation from the backend user
        picking_receipt.button_validate()
        self.assertEqual(mo.state, 'done')
        self.assertEqual(mo.move_line_raw_ids[0].quantity, 1)
        self.assertEqual(mo.move_line_raw_ids[0].lot_id, serial2)
        self.assertEqual(mo.move_line_raw_ids[1].quantity, 1)
        self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3)
        self.assertEqual(mo.move_line_raw_ids[2].quantity, 2)
class TestSubcontractingSerialMassReceipt(TransactionCase):

    def setUp(self):
        super().setUp()
        self.subcontractor = self.env['res.partner'].create({
            'name': 'Subcontractor',
        })
        self.resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
        self.raw_material = self.env['product.product'].create({
            'name': 'Component',
            'is_storable': True,
            'route_ids': [Command.link(self.resupply_route.id)],
        })
        self.finished = self.env['product.product'].create({
            'name': 'Finished',
            'is_storable': True,
            'tracking': 'serial'
        })
        self.bom = self.env['mrp.bom'].create({
            'product_id': self.finished.id,
            'product_tmpl_id': self.finished.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'subcontract',
            'subcontractor_ids': [Command.link(self.subcontractor.id)],
            'consumption': 'strict',
            'bom_line_ids': [
                Command.create({'product_id': self.raw_material.id, 'product_qty': 1}),
            ]
        })

    def test_receive_after_resupply(self):
        quantities = [5, 4, 1]
        # Make needed component stock
        self.env['stock.quant']._update_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock'), sum(quantities))
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = sum(quantities)
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        # Process the delivery of the components
        picking_deliver = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)]).picking_ids
        picking_deliver.action_assign()
        picking_deliver.button_validate()
        # Receive
        for quantity in quantities:
            # Receive <quantity> finished products
            picking_receipt.do_unreserve()
            lot_name = self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1'
            picking_receipt.move_ids[0]._generate_serial_numbers(lot_name, quantity)
            picking_receipt.move_ids.picked = True
            wizard_data = picking_receipt.button_validate()
            if wizard_data is not True:
                # Create backorder
                Form.from_action(self.env, wizard_data).save().process()
                self.assertEqual(picking_receipt.state, 'done')
                picking_receipt = picking_receipt.backorder_ids[-1]
                self.assertEqual(picking_receipt.state, 'assigned')
        self.assertEqual(picking_receipt.state, 'done')
        self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
        self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor), 0)

    def test_receive_no_resupply(self):
        quantity = 5
        # Create a receipt picking from the subcontractor
        picking_form = Form(self.env['stock.picking'])
        picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
        picking_form.partner_id = self.subcontractor
        with picking_form.move_ids_without_package.new() as move:
            move.product_id = self.finished
            move.product_uom_qty = quantity
        picking_receipt = picking_form.save()
        picking_receipt.action_confirm()
        picking_receipt.do_unreserve()
        # Receive finished products
        lot_name = self.env['stock.lot']._get_next_serial(picking_receipt.company_id, picking_receipt.move_ids[0].product_id) or 'sn#1'
        picking_receipt.move_ids[0]._generate_serial_numbers(lot_name, quantity)
        picking_receipt.move_ids.picked = True
        picking_receipt.button_validate()
        self.assertEqual(picking_receipt.state, 'done')
        self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.env.ref('stock.stock_location_stock')), 0)
        self.assertEqual(self.env['stock.quant']._get_available_quantity(self.raw_material, self.subcontractor.property_stock_subcontractor, allow_negative=True), -quantity)

    def test_bom_subcontracting_product_dynamic_attribute(self):
        """
            Test that the report BOM data is available for a product with an dynamic attribute
            but without variant.
        """
        dynamic_attribute = self.env['product.attribute'].create({
            'name': 'flavour',
            'create_variant': 'dynamic',
        })
        value_1 = self.env['product.attribute.value'].create({
            'name': 'Vanilla',
            'attribute_id': dynamic_attribute.id,
        })
        value_2 = self.env['product.attribute.value'].create({
            'name': 'Chocolate',
            'attribute_id': dynamic_attribute.id,
        })
        product_template = self.env['product.template'].create({
            'name': 'Cake',
            'uom_id': self.env.ref('uom.product_uom_unit').id,
            'is_storable': True,
        })
        self.env['product.template.attribute.line'].create({
            'product_tmpl_id': product_template.id,
            'attribute_id': dynamic_attribute.id,
            'value_ids': [Command.set([value_1.id, value_2.id])],
        })
        bom = self.env['mrp.bom'].create({
            'product_tmpl_id': product_template.id,
            'type': 'subcontract',
            'subcontractor_ids': [Command.set([self.subcontractor.id])],
        })
        report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom.id, searchVariant=False)
        self.assertTrue(report_values)
