# Part of Odoo. See LICENSE file for full copyright and licensing details.
""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """

from odoo.fields import Command
from odoo.addons.mrp_account.tests.common import TestBomPriceCommon
from odoo.tests import Form, tagged
from odoo.tests.common import new_test_user

PRICE = 718.75 - 100  # total price minus glass


@tagged('at_install', '-post_install')  # LEGACY at_install
class TestMrpValuationStandard(TestBomPriceCommon):
    def _get_production_cost_move_lines(self):
        return self.env['account.move.line'].search([
            ('account_id', '=', self.account_production.id),
        ], order='date, id')

    def test_mo_journal_entry_ref_matches_mo_name(self):
        self.glass.categ_id = self.category_fifo_auto

        self._make_in_move(self.glass, 1, 10)
        mo = self._create_mo(self.bom_1, 1)
        self._produce(mo)
        mo.button_mark_done()

        account_moves = (mo.move_raw_ids + mo.move_finished_ids).account_move_id
        self.assertTrue(account_moves, "Expected accounting entries to be created for the manufacturing order.")
        self.assertEqual(set(account_moves.mapped('ref')), {mo.name})

    def test_fifo_fifo_1(self):
        self.glass.categ_id = self.category_fifo
        self.dining_table.categ_id = self.category_fifo

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        action = mo.button_mark_done()
        backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
        backorder.save().action_backorder()
        mo = mo.production_group_id.production_ids[-1]
        self.assertEqual(self.glass.total_value, 20)
        self.assertEqual(self.dining_table.total_value, PRICE + 10)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)

    def test_fifo_fifo_2(self):
        self.glass.categ_id = self.category_fifo

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)
        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, (2 * PRICE + 10 + 20) / 2)

    def test_fifo_unbuild(self):
        """ This test creates an MO and then creates an unbuild
        orders and checks the stock valuation.
        """
        self.glass.categ_id = self.category_fifo
        # ---------------------------------------------------
        #       MO
        # ---------------------------------------------------
        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 1)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 20)
        # ---------------------------------------------------
        #       Unbuild
        # ---------------------------------------------------
        unbuild_form = Form(self.env['mrp.unbuild'])
        unbuild_form.mo_id = mo
        unbuild_form.save().action_unbuild()
        self.assertEqual(self.glass.total_value, 30)

    def test_fifo_produce_deliver_return_unbuild(self):
        self.glass.categ_id = self.category_fifo
        self._make_in_move(self.glass, 1, 10)

        mo = self._create_mo(self.bom_1, 1)
        self._produce(mo)
        mo.button_mark_done()

        out_move = self._make_out_move(self.dining_table, 1.0, create_picking=True)
        self._make_return(out_move, 1.0)

        unbuild_form = Form(self.env['mrp.unbuild'])
        unbuild_form.mo_id = mo
        unbuild_form.save().action_unbuild()

        moves = self.env['stock.move'].search([('product_id', '=', self.dining_table.id)])
        self.assertRecordValues(moves, [
            {'value': PRICE + 10, 'quantity': 1.0, 'is_in': True, 'remaining_value': 0.0, 'remaining_qty': 0.0},
            {'value': PRICE + 10, 'quantity': 1.0, 'is_in': False, 'remaining_value': 0.0, 'remaining_qty': 0.0},
            {'value': PRICE + 10, 'quantity': 1.0, 'is_in': True, 'remaining_value': 0.0, 'remaining_qty': 0.0},
            {'value': PRICE + 10, 'quantity': 1.0, 'is_in': False, 'remaining_value': 0.0, 'remaining_qty': 0.0},
        ])

    def test_fifo_avco_1(self):
        self.glass.categ_id = self.category_fifo
        self.dining_table.categ_id = self.category_avco

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        action = mo.button_mark_done()
        backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
        backorder.save().action_backorder()
        mo = mo.production_group_id.production_ids[-1]
        self.assertEqual(self.glass.total_value, 20)
        self.assertEqual(self.dining_table.total_value, PRICE + 10)
        self._produce(mo)

        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * PRICE + 10 + 20)

    def test_fifo_avco_2(self):
        self.glass.categ_id = self.category_fifo
        self.dining_table.categ_id = self.category_avco
        self.dining_table.categ_id = self.category_fifo

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, PRICE * 2 + 10 + 20)
        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, (PRICE * 2 + 10 + 20) / 2)

    def test_fifo_std_1(self):
        self.glass.categ_id = self.category_fifo
        self.dining_table.categ_id = self.category_standard
        self.dining_table.standard_price = 8.8

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        mo._post_inventory()
        self.assertEqual(self.glass.total_value, 20)
        self.assertEqual(self.dining_table.total_value, 8.8)
        self._produce(mo)
        warning = Form.from_action(self.env, mo.button_mark_done()).save()
        for warning_line in warning.mrp_consumption_warning_line_ids:
            self.assertEqual(warning_line.product_expected_qty_uom, 2 * warning_line.product_consumed_qty_uom)
        warning.action_confirm()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 8.8 * 2)

    def test_fifo_std_2(self):
        self.glass.categ_id = self.category_fifo
        self.dining_table.categ_id = self.category_standard
        self.dining_table.standard_price = 8.8

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 8.8 * 2)
        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, 8.8)

    def test_std_avco_1(self):
        self.glass.categ_id = self.category_standard
        self.dining_table.categ_id = self.category_avco

        self._make_in_move(self.glass, 1)
        self._make_in_move(self.glass, 1)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        mo._post_inventory()
        self.assertEqual(self.glass.total_value, 100)
        self.assertEqual(self.dining_table.total_value, PRICE + 100)
        self._produce(mo)
        warning = Form.from_action(self.env, mo.button_mark_done()).save()
        for warning_line in warning.mrp_consumption_warning_line_ids:
            self.assertEqual(warning_line.product_expected_qty_uom, 2 * warning_line.product_consumed_qty_uom)
        warning.action_confirm()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * (PRICE + 100))

    def test_std_avco_2(self):
        self.glass.categ_id = self.category_standard
        self.dining_table.categ_id = self.category_avco

        self._make_in_move(self.glass, 1)
        self._make_in_move(self.glass, 1)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * (PRICE + 100))
        self.assertEqual(self.dining_table.standard_price, PRICE + 100)

        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, PRICE + 100)

        # Update component price
        self.glass.standard_price = 0

        self._make_in_move(self.glass, 3)
        mo = self._create_mo(self.bom_1, 3)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.dining_table.total_value, 4 * PRICE + 100)
        self.assertEqual(self.dining_table.standard_price, (4 * PRICE + 100) / 4)

    def test_std_std_1(self):
        self.glass.categ_id = self.category_standard
        self.dining_table.categ_id = self.category_standard

        self._make_in_move(self.glass, 1)
        self._make_in_move(self.glass, 1)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        mo._post_inventory()
        self.assertEqual(self.glass.total_value, 100)
        self.assertEqual(self.dining_table.total_value, 1000)
        self._produce(mo)
        warning = Form.from_action(self.env, mo.button_mark_done()).save()
        for warning_line in warning.mrp_consumption_warning_line_ids:
            self.assertEqual(warning_line.product_expected_qty_uom, 2 * warning_line.product_consumed_qty_uom)
        warning.action_confirm()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2000)

    def test_std_std_2(self):
        self.glass.categ_id = self.category_standard
        self.dining_table.categ_id = self.category_standard

        self._make_in_move(self.glass, 1)
        self._make_in_move(self.glass, 1)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2000)
        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, 1000)

    def test_avco_avco_1(self):
        self.glass.categ_id = self.category_avco
        self.dining_table.categ_id = self.category_avco

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo, 1)
        mo._post_inventory()
        self.assertEqual(self.glass.total_value, 15)
        self.assertEqual(self.dining_table.total_value, PRICE + 15)
        self._produce(mo)
        warning = Form.from_action(self.env, mo.button_mark_done()).save()
        for warning_line in warning.mrp_consumption_warning_line_ids:
            self.assertEqual(warning_line.product_expected_qty_uom, 2 * warning_line.product_consumed_qty_uom)
        warning.action_confirm()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * PRICE + 30)

    def test_avco_avco_2(self):
        self.glass.categ_id = self.category_avco
        self.dining_table.categ_id = self.category_avco

        self._make_in_move(self.glass, 1, 10)
        self._make_in_move(self.glass, 1, 20)
        mo = self._create_mo(self.bom_1, 2)
        self._produce(mo)
        mo.button_mark_done()
        self.assertEqual(self.glass.total_value, 0)
        self.assertEqual(self.dining_table.total_value, 2 * PRICE + 30)
        self._make_out_move(self.dining_table, 1)
        self.assertEqual(self.dining_table.total_value, (2 * PRICE + 30) / 2)

    def test_validate_draft_kit(self):
        """
        Create a draft receipt, add a kit to its move lines and directly
        validate it. From client side, such a behaviour is possible with
        the Barcode app.
        """
        self.plywood_sheet.qty_available = 0
        self.plywood_sheet.categ_id = self.category_avco

        receipt = self.env['stock.picking'].create({
            'location_id': self.customer_location.id,
            'location_dest_id': self.stock_location.id,
            'picking_type_id': self.picking_type_in.id,
            'state': 'draft',
            'move_line_ids': [(0, 0, {
                'product_id': self.table_head.id,
                'quantity': 12,
                'uom_id': self.table_head.uom_id.id,
                'location_id': self.customer_location.id,
                'location_dest_id': self.stock_location.id,
            })],
        })
        receipt.move_ids.picked = True
        receipt.button_validate()

        self.assertEqual(self.plywood_sheet.qty_available, 12)
        self.assertEqual(self.plywood_sheet.total_value, 2400)

    def test_production_account_00(self):
        """Create move into/out of a production location, test we create account
        entries with the Production Cost account.
        """
        self.dining_table.categ_id.property_cost_method = 'standard'

        # move into production location
        self._make_out_move(self.dining_table, 1, location_dest_id=self.prod_location.id)

        in_aml = self._get_production_cost_move_lines()
        self.assertEqual(in_aml.debit, 1000)
        self.assertEqual(in_aml.product_id, self.dining_table)

        # move out of production location
        self._make_in_move(self.dining_table, 1, location_id=self.prod_location.id)

        out_aml = self._get_production_cost_move_lines() - in_aml
        self.assertEqual(out_aml.credit, 1000)
        self.assertEqual(in_aml.product_id, self.dining_table)

    def test_average_cost_unbuild_component_change_move_qty(self):
        """
        Ensures that we can modify the quantity on the stock move of the components after an unbuild
        """
        mo = self._create_mo(self.bom_1, 1)
        self._produce(mo)
        mo.button_mark_done()
        action = mo.button_unbuild()
        wizard = Form(self.env[action['res_model']].with_context(action['context']))
        wizard.product_qty = 1
        unbuild = wizard.save()
        unbuild.action_validate()
        # check that changing the quantity on the move form does not create an error
        comp_move = mo.unbuild_ids.produce_line_ids.filtered(lambda move: move.product_id.id == self.glass.id)
        with Form(comp_move.move_line_ids[0]) as form:
            form.quantity = 0

    def test_kit_product_valuation(self):
        """
        Verify that kit products are excluded from inventory valuation,
        do not affect valuation when their price changes, and still appear
        in the quantity history report with total value of zero.
        """
        self.assertRecordValues(self.table_head, [{'standard_price': 300, 'total_value': 0}])
        self.assertTrue(self.table_head not in self.env.company._get_accounts_by_product())
        old_stock_value = sum(self.env.company.stock_value().values())
        self.table_head.action_bom_cost()
        self.assertRecordValues(self.table_head, [{'standard_price': 468.75, 'total_value': 0}])
        self.assertEqual(old_stock_value, sum(self.env.company.stock_value().values()))
        action = self.env['stock.quantity.history'].create({}).open_at_date()
        products = self.env[action['res_model']].with_context(action['context']).search(action['domain'])
        self.assertRecordValues(
            products & self.table_head,
            [{
                'standard_price': 468.75,
                'qty_available': 1,
                'total_value': 0,
                'avg_cost': 0,
            }],
        )

    def test_mo_valuation_uses_production_company(self):
        """
        Verify that when validating an MO while env.company differs from mo.company_id, we read company-dependent
        fields (product.cost_method / standard_price) in the MO's company, not the caller's env.company
        """
        self._make_in_move(self.glass, 1, 10)
        mo = self._create_mo(self.bom_1, 1)
        self._produce(mo)
        mo.with_company(self.other_company).button_mark_done()
        self.assertEqual(self.dining_table.total_value, PRICE + 10)

    def test_kit_in_other_company_does_not_zero_valuation(self):
        """
        Check that a product's stock value and on-hand quantity in one company are
        unaffected by a kit defined for the same product in another company.
        """
        company_1 = self.company
        company_2 = self.other_company

        super_product, component = self.env['product.product'].create([
            {'name': 'Super product', 'is_storable': True, 'categ_id': self.category_standard.id},
            {'name': 'Component', 'is_storable': True, 'categ_id': self.category_standard.id},
        ])

        super_product.with_company(company_1).standard_price = 50
        self.env['stock.quant']._update_available_quantity(super_product, self.stock_location, 10)

        self.env['mrp.bom'].with_company(company_2).create({
            'product_id': super_product.id,
            'product_tmpl_id': super_product.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'phantom',
            'company_id': company_2.id,
            'bom_line_ids': [Command.create({'product_id': component.id, 'product_qty': 1})],
        })
        super_product.invalidate_recordset()

        super_product = super_product.with_context(allowed_company_ids=(company_1 + company_2).ids)
        self.assertEqual(super_product.total_value, 500)
        self.assertEqual(super_product.qty_available, 10)

    def test_validate_branch_mo_with_main_company_component(self):
        """
        Check that an MRP user of a branch company can process an MO in a multi-company
        setup where the component belongs to the main company.
        """
        sub_company = self.branch
        self.category_avco.with_company(self.branch).write({'property_valuation': 'real_time', 'property_cost_method': 'average'})
        final_product, component = self.env['product.product'].create([
            {
                'name': 'Finished Product',
                'is_storable': True,
                'company_id': self.company.id,
                'categ_id': self.category_avco.id,
            },
            {
                'name': 'Component',
                'is_storable': True,
                'company_id': self.company.id,
                'categ_id': self.category_avco.id,
            },
        ])
        bom = self.env['mrp.bom'].create({
            'product_id': final_product.id,
            'product_tmpl_id': final_product.product_tmpl_id.id,
            'product_qty': 1.0,
            'type': 'normal',
            'company_id': sub_company.id,
            'bom_line_ids': [Command.create({'product_id': component.id, 'product_qty': 2})],
        })
        self._make_in_move(component, 1, 10, company=self.branch)
        self.assertEqual(component.with_company(self.branch).standard_price, 10.0)
        mrp_user = new_test_user(
            self.env,
            login='test_mrp_sub_user',
            groups='mrp.group_mrp_user,stock.group_stock_user',
            company_id=sub_company.id,
        )
        mo = self.env['mrp.production'].with_context(allowed_company_ids=sub_company.ids).with_user(mrp_user).create({
            'product_id': final_product.id,
            'bom_id': bom.id,
            'product_qty': 1.0,
        })
        mo.action_confirm()
        with Form(mo) as mo_form:
            mo_form.qty_producing = 1.0
        mo.button_mark_done()
        self.assertEqual(mo.state, 'done')
        self.assertEqual(final_product.with_company(self.branch).standard_price, 20.0)
