""" Implementation of "INVENTORY VALUATION TESTS" spreadsheet. """

from datetime import timedelta
from freezegun import freeze_time

from odoo import fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.stock_account.tests.common import TestStockValuationCommon
from odoo.exceptions import ValidationError
from odoo.tests import Form, tagged


class TestStockValuationStandard(TestStockValuationCommon):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.product = cls.product_standard

    def test_normal_1(self):
        self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 15)

        self.assertEqual(self.product.total_value, 50)
        self.assertEqual(self.product.qty_available, 5)

    def test_change_in_past_increase_in_1(self):
        move1 = self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 15)
        move1.move_line_ids.quantity = 15

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

    def test_change_in_past_decrease_in_1(self):
        move1 = self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 15)
        move1.move_line_ids.quantity = 5

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_change_in_past_add_ml_in_1(self):
        move1 = self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 15)
        self.env['stock.move.line'].create({
            'move_id': move1.id,
            'product_id': move1.product_id.id,
            'quantity': 5,
            'product_uom_id': move1.product_uom.id,
            'location_id': move1.location_id.id,
            'location_dest_id': move1.location_dest_id.id,
        })

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

    def test_change_in_past_increase_out_1(self):
        self._make_in_move(self.product, 10)
        move2 = self._make_out_move(self.product, 1)
        move2.move_line_ids.quantity = 5

        self.assertEqual(self.product.total_value, 50)
        self.assertEqual(self.product.qty_available, 5)

    def test_change_in_past_decrease_out_1(self):
        self._make_in_move(self.product, 10)
        move2 = self._make_out_move(self.product, 5)
        move2.move_line_ids.quantity = 1

        self.assertEqual(self.product.total_value, 90)
        self.assertEqual(self.product.qty_available, 9)

    def test_change_standard_price_1(self):
        self._make_out_move(self.product, 15)
        self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)

        # change cost from 10 to 15
        self.product.standard_price = 15.0

        self.assertEqual(self.product.total_value, 75)
        self.assertEqual(self.product.qty_available, 5)
        self.assertEqual(self.product.avg_cost, 15)

    def test_negative_1(self):
        move1 = self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 15)
        self.env['stock.move.line'].create({
            'move_id': move1.id,
            'product_id': move1.product_id.id,
            'quantity': 10,
            'product_uom_id': move1.product_uom.id,
            'location_id': move1.location_id.id,
            'location_dest_id': move1.location_dest_id.id,
        })

        self.assertEqual(self.product.total_value, 50)
        self.assertEqual(self.product.qty_available, 5)

    def test_dropship_1(self):
        self._make_dropship_move(self.product, 10)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_change_in_past_increase_dropship_1(self):
        move1 = self._make_dropship_move(self.product, 10)
        move1.move_line_ids.quantity = 15

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_empty_stock_move_valuation(self):
        product1 = self.env['product.product'].create({
            'name': 'p1',
            'is_storable': True,
            'categ_id': self.env.ref('product.product_category_expenses').id,
        })
        product2 = self.env['product.product'].create({
            'name': 'p2',
            'is_storable': True,
            'categ_id': self.env.ref('product.product_category_expenses').id,
        })
        picking = self.env['stock.picking'].create({
            'picking_type_id': self.picking_type_in.id,
            'location_id': self.supplier_location.id,
            'location_dest_id': self.stock_location.id,
        })
        for product in (product1, product2):
            product.standard_price = 10
            in_move = self.env['stock.move'].create({
                'product_id': product.id,
                'location_id': self.supplier_location.id,
                'location_dest_id': self.stock_location.id,
                'product_uom': self.uom.id,
                'product_uom_qty': 2,
                'price_unit': 10,
                'picking_type_id': self.picking_type_in.id,
                'picking_id': picking.id
            })

        picking.action_confirm()
        # set quantity done only on one move
        in_move.move_line_ids.quantity = 2
        in_move.picked = True
        res_dict = picking.button_validate()
        wizard = self.env[(res_dict.get('res_model'))].with_context(res_dict.get('context')).browse(res_dict.get('res_id'))
        wizard.process()

        self.assertEqual(product1.total_value, 0)
        self.assertEqual(product2.total_value, 20)

    def test_currency_precision_and_standard_value(self):
        currency = self.env['res.currency'].create({
            'name': 'Odoo',
            'symbol': 'O',
            'rounding': 1,
        })
        new_company = self.env['res.company'].create({
            'name': 'Super Company',
            'currency_id': currency.id,
        })

        old_company = self.env.user.company_id
        try:
            self.env.user.company_id = new_company
            product = self.product.with_company(new_company)
            product.standard_price = 3

            self._make_in_move(product, 0.5, company=new_company)
            self._make_out_move(product, 0.5, company=new_company)

            self.assertEqual(product.total_value, 0.0)
        finally:
            self.env.user.company_id = old_company

    def test_multicompany(self):
        """Standard: total_value = standard_price * qty, isolated per company."""
        self.product.with_company(self.company).standard_price = 10
        self.product.with_company(self.other_company).standard_price = 50

        # Company 1: receive 15 units at 10$
        self._make_in_move(self.product, 15)
        # Company 2: receive 100 units at 50$
        self._make_in_move(self.product, 100, unit_cost=50, company=self.other_company)

        # Company 1 only: 15 units @ 10$ = 150$
        product_company_1 = self.product.with_company(self.company).with_context(allowed_company_ids=self.company.ids)
        self.assertEqual(product_company_1.qty_available, 15)
        self.assertEqual(product_company_1.total_value, 150)

        # Company 2 only: 100 units @ 50$ = 5000$
        product_company_2 = self.product.with_company(self.other_company).with_context(allowed_company_ids=self.other_company.ids)
        self.assertEqual(product_company_2.qty_available, 100)
        self.assertEqual(product_company_2.total_value, 5000)

        # Both companies: 115 units, 5150$
        # Invalidate so the ORM doesn't return the c1-keyed cached value (same
        # cache key as with_company(c1) since depends_context('company') maps
        # to env.company.id = first entry of allowed_company_ids).
        self.product.invalidate_recordset(['total_value'])
        product_both = self.product.with_context(allowed_company_ids=(self.company | self.other_company).ids)
        self.assertEqual(product_both.qty_available, 115)
        self.assertEqual(product_both.total_value, 5150)

    def test_change_qty_and_locations_of_done_sml(self):
        sub_stock_loc = self.env['stock.location'].create({
            'name': 'shelf1',
            'usage': 'internal',
            'location_id': self.stock_location.id,
        })

        move_in = self._make_in_move(self.product, 25)
        self.assertEqual(self.product.total_value, 250)
        self.assertEqual(self.product.qty_available, 25)

        move_in.move_line_ids.write({
            'location_dest_id': sub_stock_loc.id,
            'quantity': 30,
        })
        self.assertEqual(self.product.total_value, 300)
        self.assertEqual(self.product.qty_available, 30)

        sub_loc_quant = self.product.stock_quant_ids.filtered(lambda q: q.location_id == sub_stock_loc)
        self.assertEqual(sub_loc_quant.quantity, 30)


class TestStockValuationAVCO(TestStockValuationCommon):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.product = cls.product_avco

    def test_normal_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self.assertEqual(self.product.standard_price, 10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self.assertEqual(self.product.standard_price, 15)
        self._make_out_move(self.product, 15)
        self.assertEqual(self.product.standard_price, 15)

        self.assertEqual(self.product.total_value, 75)
        self.assertEqual(self.product.qty_available, 5)

    def test_change_in_past_increase_in_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 15)
        self._set_quantity(move1, 15)

        self.assertEqual(self.product.total_value, 140)
        self.assertEqual(self.product.qty_available, 10)
        self.assertEqual(self.product.standard_price, 14)

    def test_change_in_past_decrease_in_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 15)
        self._set_quantity(move1, 5)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_change_in_past_add_ml_in_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 15)
        self._add_move_line(move1, quantity=5)

        self.assertEqual(self.product.total_value, 140)
        self.assertEqual(self.product.qty_available, 10)
        self.assertEqual(self.product.standard_price, 14)

    def test_change_in_past_add_move_in_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10, create_picking=True)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 15)
        self._add_move_line(move1, quantity=5, state='done', picking_id=move1.picking_id.id)

        self.assertEqual(self.product.total_value, 140)
        self.assertEqual(self.product.qty_available, 10)
        self.assertEqual(self.product.standard_price, 14)

    def test_change_in_past_increase_out_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        move3 = self._make_out_move(self.product, 15)
        self._set_quantity(move3, 20)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)
        self.assertEqual(self.product.standard_price, 15)

    def test_change_in_past_decrease_out_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        move3 = self._make_out_move(self.product, 15)
        self._set_quantity(move3, 10)

        self.assertEqual(self.product.total_value, 150)
        self.assertEqual(self.product.qty_available, 10)
        self.assertEqual(self.product.standard_price, 15)

    def test_negative_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 30)
        self._make_in_move(self.product, 10, unit_cost=30)
        self._make_in_move(self.product, 10, unit_cost=40)

        self.assertEqual(self.product.total_value, 400)
        self.assertEqual(self.product.qty_available, 10)

    def test_negative_2(self):
        self.product.standard_price = 10
        self._make_out_move(self.product, 1, force_assign=True)
        self._make_in_move(self.product, 1, unit_cost=15)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    @freeze_time("2026-03-10 10:00:00")
    def test_return_receipt_1(self):
        """
        Receive a product twice, return one unit and deliver the other one.
        The total value should be 0 and the price should be (10+20)/2 = 15.
        Return the delivery and set the original quantity to 0.
        The total value should be 15.
        """
        move1 = self._make_in_move(self.product, 1, unit_cost=10, create_picking=True)
        self._make_in_move(self.product, 1, unit_cost=20)
        move2 = self._make_out_move(self.product, 1)
        self._make_return(move1, 1)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)
        self.assertEqual(self.product.standard_price, 15)

        self._make_return(move2, 1)
        move2.quantity = 0
        self.assertEqual(self.product.with_context(to_date=fields.Datetime.now() + timedelta(days=1)).total_value, 15.0)

    def test_return_delivery_1(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self._make_in_move(self.product, 1, unit_cost=20)
        move3 = self._make_out_move(self.product, 1, create_picking=True)
        self._make_return(move3, 1)

        self.assertEqual(self.product.total_value, 30)
        self.assertEqual(self.product.qty_available, 2)
        self.assertEqual(self.product.standard_price, 15)

    def test_rereturn_receipt_1(self):
        move1 = self._make_in_move(self.product, 1, unit_cost=10, create_picking=True)
        self._make_in_move(self.product, 1, unit_cost=20)
        self._make_out_move(self.product, 1)
        move4 = self._make_return(move1, 1)  # -15, current avco
        self._make_return(move4, 1)  # +10, original move's price unit

        self.assertEqual(self.product.total_value, 15)
        self.assertEqual(self.product.qty_available, 1)
        self.assertEqual(self.product.standard_price, 15)

    def test_rereturn_delivery_1(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self._make_in_move(self.product, 1, unit_cost=20)
        move3 = self._make_out_move(self.product, 1, create_picking=True)
        move4 = self._make_return(move3, 1)
        self._make_return(move4, 1)

        self.assertEqual(self.product.total_value, 15)
        self.assertEqual(self.product.qty_available, 1)
        self.assertEqual(self.product.standard_price, 15)

    def test_dropship_1(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self._make_in_move(self.product, 1, unit_cost=20)
        self._make_dropship_move(self.product, 1, unit_cost=10)

        self.assertEqual(self.product.total_value, 26.67)
        self.assertEqual(self.product.qty_available, 2)
        self.assertAlmostEqual(self.product.standard_price, 13.33, places=2)

    def test_rounding_1(self):
        self._make_in_move(self.product, 1, unit_cost=1.00)
        self._make_in_move(self.product, 1, unit_cost=1.00)
        self._make_in_move(self.product, 1, unit_cost=1.01)

        self.assertAlmostEqual(self.product.total_value, 3.01)

        self._make_out_move(self.product, 3, create_picking=True)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)
        self.assertAlmostEqual(self.product.standard_price, 1.0033333)

    def test_rounding_2(self):
        self._make_in_move(self.product, 1, unit_cost=1.02)
        self._make_in_move(self.product, 1, unit_cost=1.00)
        self._make_in_move(self.product, 1, unit_cost=1.00)

        self.assertAlmostEqual(self.product.total_value, 3.02)

        self._make_out_move(self.product, 3, create_picking=True)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)
        self.assertAlmostEqual(self.product.standard_price, 1.00666666)

    def test_rounding_3(self):
        self._make_in_move(self.product, 1000, unit_cost=0.17)
        self._make_in_move(self.product, 800, unit_cost=0.23)

        self.assertAlmostEqual(self.product.standard_price, 0.19666666)

        self._make_out_move(self.product, 1000, create_picking=True)
        self._make_out_move(self.product, 800, create_picking=True)

        self.assertEqual(self.product.total_value, 0)

    def test_rounding_4(self):
        """
        The first 2 In moves result in a rounded standard_price at 3.4943, which is rounded at 3.49.
        This test ensures that no rounding error is generated with small out quantities.
        """
        self._make_in_move(self.product, 2, unit_cost=4.63)
        self._make_in_move(self.product, 5, unit_cost=3.04)
        self.assertAlmostEqual(self.product.standard_price, 3.49428571)

        for _ in range(70):
            self._make_out_move(self.product, 0.1)

        self.assertEqual(self.product.qty_available, 0)
        self.assertEqual(self.product.total_value, 0)

    def test_rounding_5(self):
        self._make_in_move(self.product, 10, unit_cost=16.83)
        self._make_in_move(self.product, 10, unit_cost=20)
        self.assertEqual(self.product.standard_price, 18.415)

        self._make_out_move(self.product, 10)
        out_move = self._make_out_move(self.product, 9)
        self.assertEqual(out_move.value, 165.74)

        self.assertEqual(self.product.total_value, 18.42)
        self.assertEqual(self.product.qty_available, 1)

        self._make_out_move(self.product, 1)
        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_return_delivery_2(self):
        self.product.standard_price = 1
        move1 = self._make_out_move(self.product, 10, create_picking=True, force_assign=True)
        self._make_in_move(self.product, 10, unit_cost=2)
        self._make_return(move1, 10)

        self.assertEqual(self.product.total_value, 10)
        self.assertEqual(self.product.qty_available, 10)
        self.assertEqual(self.product.standard_price, 1)

    def test_multicompany(self):
        """AVCO: standard_price auto-updates per company; total_value is isolated."""
        self.category_avco.with_company(self.other_company).property_cost_method = 'average'

        # Company 1: receive 15 units at 10$ → AVCO = 10, value = 150$
        self._make_in_move(self.product, 15, unit_cost=10)
        # Company 2: receive 100 units at 50$ → AVCO = 50, value = 5000$
        self._make_in_move(self.product, 100, unit_cost=50, company=self.other_company)

        # Company 1 only: 15 units @ 10$ = 150$
        product_company_1 = self.product.with_company(self.company).with_context(allowed_company_ids=self.company.ids)
        self.assertEqual(product_company_1.qty_available, 15)
        self.assertEqual(product_company_1.total_value, 150)

        # Company 2 only: 100 units @ 50$ = 5000$
        product_company_2 = self.product.with_company(self.other_company).with_context(allowed_company_ids=self.other_company.ids)
        self.assertEqual(product_company_2.qty_available, 100)
        self.assertEqual(product_company_2.total_value, 5000)

        # Both companies: 115 units, 5150$
        self.product.invalidate_recordset(['total_value'])
        product_both = self.product.with_context(allowed_company_ids=(self.company | self.other_company).ids)
        self.assertEqual(product_both.qty_available, 115)
        self.assertEqual(product_both.total_value, 5150)

    def test_return_delivery_rounding(self):
        self._make_in_move(self.product, 1, unit_cost=13.13)
        self._make_in_move(self.product, 1, unit_cost=12.20)
        move3 = self._make_out_move(self.product, 2, create_picking=True)
        move4 = self._make_return(move3, 2)

        self.assertAlmostEqual(abs(move3.value), abs(move4.value))
        self.assertAlmostEqual(self.product.total_value, 25.33)
        self.assertEqual(self.product.qty_available, 2)


class TestStockValuationFIFO(TestStockValuationCommon):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.product = cls.product_fifo

    def test_normal_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 15)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 5)

    def test_negative_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 30)
        self.assertEqual(self.product.qty_available, -10)
        self._make_in_move(self.product, 10, unit_cost=30)
        self.assertEqual(self.product.qty_available, 0)
        self._make_in_move(self.product, 10, unit_cost=40)

        self.assertEqual(self.product.total_value, 400)
        self.assertEqual(self.product.qty_available, 10)

    def test_change_in_past_decrease_in_1(self):
        move1 = self._make_in_move(self.product, 20, unit_cost=10)
        self._make_out_move(self.product, 10)
        self._set_quantity(move1, 10)

        self.assertEqual(self.product.total_value, 0)
        self.assertEqual(self.product.qty_available, 0)

    def test_change_in_past_decrease_in_2(self):
        move1 = self._make_in_move(self.product, 20, unit_cost=10)
        self._make_out_move(self.product, 10)
        self._make_out_move(self.product, 10)
        self._set_quantity(move1, 10)
        self._make_in_move(self.product, 20, unit_cost=15)

        self.assertEqual(self.product.total_value, 150)
        self.assertEqual(self.product.qty_available, 10)

    def test_change_in_past_increase_in_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=15)
        self._make_out_move(self.product, 20)
        self._set_quantity(move1, 20)

        self.assertEqual(self.product.total_value, 150)
        self.assertEqual(self.product.qty_available, 10)

    def test_change_in_past_increase_in_2(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=12)
        self._make_out_move(self.product, 15)
        self._make_out_move(self.product, 20)
        self._make_in_move(self.product, 100, unit_cost=15)
        self._set_quantity(move1, 20)

        self.assertEqual(self.product.total_value, 1425)
        self.assertEqual(self.product.qty_available, 95)

    def test_change_in_past_increase_out_1(self):
        self._make_in_move(self.product, 20, unit_cost=10)
        move2 = self._make_out_move(self.product, 10)
        self._make_in_move(self.product, 20, unit_cost=15)
        self._set_quantity(move2, 25)

        self.assertEqual(self.product.total_value, 225)
        self.assertEqual(self.product.qty_available, 15)

    def test_change_in_past_decrease_out_1(self):
        """ Decrease the quantity of an outgoing stock.move.line will act like
        an inventory adjustement and not a return. It will take the move value
        in order to set the value and not the standard price of the product.
        """
        self._make_in_move(self.product, 20, unit_cost=10)
        move2 = self._make_out_move(self.product, 15)
        self._make_in_move(self.product, 20, unit_cost=15)
        self._set_quantity(move2, 5)

        self.assertEqual(self.product.total_value, 450)
        self.assertEqual(self.product.qty_available, 35)

    def test_change_in_past_add_ml_out_1(self):
        self._make_in_move(self.product, 20, unit_cost=10)
        move2 = self._make_out_move(self.product, 10)
        self._make_in_move(self.product, 20, unit_cost=15)
        self._add_move_line(move2, quantity=5)

        self.assertEqual(self.product.total_value, 350)
        self.assertEqual(self.product.qty_available, 25)

    def test_return_delivery_1(self):
        self._make_in_move(self.product, 10, unit_cost=10)
        move2 = self._make_out_move(self.product, 10, create_picking=True)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_return(move2, 10)

        self.assertEqual(self.product.total_value, 300)
        self.assertEqual(self.product.qty_available, 20)

    def test_return_receipt_1(self):
        move1 = self._make_in_move(self.product, 10, unit_cost=10, create_picking=True)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_return(move1, 2)

        self.assertEqual(self.product.total_value, 280)
        self.assertEqual(self.product.qty_available, 18)

    def test_rereturn_receipt_1(self):
        move1 = self._make_in_move(self.product, 1, unit_cost=10, create_picking=True)
        self._make_in_move(self.product, 1, unit_cost=20)
        self._make_out_move(self.product, 1)
        move4 = self._make_return(move1, 1)
        self._make_return(move4, 1)

        self.assertEqual(self.product.total_value, 20)
        self.assertEqual(self.product.qty_available, 1)

    def test_rereturn_delivery_1(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self._make_in_move(self.product, 1, unit_cost=20)
        move3 = self._make_out_move(self.product, 1, create_picking=True)
        move4 = self._make_return(move3, 1)
        self._make_return(move4, 1)

        self.assertEqual(self.product.total_value, 10)
        self.assertEqual(self.product.qty_available, 1)

    def test_dropship_1(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self._make_in_move(self.product, 1, unit_cost=20)
        self._make_dropship_move(self.product, 1, unit_cost=10)

        self.assertEqual(self.product.total_value, 30)
        self.assertEqual(self.product.qty_available, 2)
        self.assertAlmostEqual(self.product.standard_price, 15)

    def test_return_delivery_2(self):
        self._make_in_move(self.product, 1, unit_cost=10)
        self.product.standard_price = 0
        self._make_in_move(self.product, 1, unit_cost=0)

        self._make_out_move(self.product, 1)
        out_move02 = self._make_out_move(self.product, 1, create_picking=True)

        returned = self._make_return(out_move02, 1)
        self.assertEqual(returned.value, 0)

    def test_return_delivery_3(self):
        self.product.standard_price = 1
        move1 = self._make_out_move(self.product, 10, create_picking=True, force_assign=True)
        self._make_in_move(self.product, 10, unit_cost=2)
        self._make_return(move1, 10)

        self.assertEqual(self.product.total_value, 10)
        self.assertEqual(self.product.qty_available, 10)

    def test_currency_precision_and_fifo_value(self):
        currency = self.env['res.currency'].create({
            'name': 'Odoo',
            'symbol': 'O',
            'rounding': 1,
        })
        new_company = self.env['res.company'].create({
            'name': 'Super Company',
            'currency_id': currency.id,
        })

        old_company = self.env.user.company_id
        try:
            self.env.user.company_id = new_company
            product = self.product.with_company(new_company)
            product.product_tmpl_id.categ_id.property_cost_method = 'fifo'

            self._make_in_move(product, 0.5, company=new_company, unit_cost=3)
            self._make_out_move(product, 0.5, company=new_company)

            self.assertEqual(product.total_value, 0.0)
        finally:
            self.env.user.company_id = old_company

    def test_fifo_avg_cost_fallback_zero_valued_qty(self):
        self.product.standard_price = 42.0
        move_in = self._make_in_move(self.product, 1)
        move_out = self._make_out_move(self.product, 1)
        move_in.move_line_ids.owner_id = self.env['res.partner'].create({'name': 'External Owner'})
        self.assertEqual(move_in._get_valued_qty(), 0.0)
        # The valued quantity is -1 since the out doesn't have an owne ter.
        self.product.invalidate_recordset(['total_value', 'qty_available'])
        self.assertEqual(self.product.total_value, -42.0)
        self.assertEqual(self.product._with_valuation_context().qty_available, -1)

        move_out.move_line_ids.owner_id = self.env['res.partner'].create({'name': 'External Owner'})
        self.product.invalidate_recordset(['total_value', 'qty_available'])
        self.assertEqual(self.product.total_value, 0.0)
        self.assertEqual(self.product.avg_cost, 42.0)

    def test_multicompany(self):
        """FIFO: value computed from each company's own move stack."""
        self.category_fifo.with_company(self.other_company).property_cost_method = 'fifo'

        # Company 1: receive 15 units at 10$
        self._make_in_move(self.product, 15, unit_cost=10)
        # Company 2: receive 100 units at 50$
        self._make_in_move(self.product, 100, unit_cost=50, company=self.other_company)

        # Company 1 only: 15 units @ 10$ = 150$
        product_company_1 = self.product.with_company(self.company).with_context(allowed_company_ids=self.company.ids)
        self.assertEqual(product_company_1.qty_available, 15)
        self.assertEqual(product_company_1.total_value, 150)

        # Company 2 only: 100 units @ 50$ = 5000$
        product_company_2 = self.product.with_company(self.other_company).with_context(
            allowed_company_ids=self.other_company.ids)
        self.assertEqual(product_company_2.qty_available, 100)
        self.assertEqual(product_company_2.total_value, 5000)
        # Both companies: 115 units, 5150$
        self.product.invalidate_recordset(['total_value'])
        product_both = self.product.with_context(allowed_company_ids=(self.company | self.other_company).ids)
        self.assertEqual(product_both.qty_available, 115)
        self.assertEqual(product_both.total_value, 5150)

    def test_fifo_consignment_valuation(self):
        owner = self.env['res.partner'].create({'name': 'External Owner'})
        self._make_in_move(self.product, 5, 10)
        self._make_in_move(self.product, 5, 20, owner_id=owner.id)
        self.assertEqual(self.product.total_value, 50.0)
        self._make_out_move(self.product, 5)
        self.assertEqual(self.product.total_value, 0.0)


class TestStockValuationChangeCostMethod(TestStockValuationCommon):
    def test_standard_to_fifo_1(self):
        """ The accounting impact of this cost method change is neutral.
        """
        self.product = self.product_standard
        self.product.product_tmpl_id.standard_price = 10

        self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
        self.assertEqual(self.product.total_value, 190)
        self.assertEqual(self.product.qty_available, 19)

    def test_standard_to_fifo_2(self):
        """ We want the same result as `test_standard_to_fifo_1` but by changing the category of
        `self.product` to another one, not changing the current one.
        """
        self.product = self.product_standard
        self.product.product_tmpl_id.standard_price = 10

        self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 1)

        cat2 = self.env['product.category'].create({'name': 'fifo', 'property_cost_method': 'fifo'})
        self.product.product_tmpl_id.categ_id = cat2
        self.assertEqual(self.product.total_value, 190)
        self.assertEqual(self.product.qty_available, 19)

    def test_avco_to_fifo(self):
        """ The accounting impact of this cost method change is neutral.
        """
        self.product = self.product_avco

        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
        self.assertEqual(self.product.total_value, 290)
        self.assertEqual(self.product.qty_available, 19)

    def test_fifo_to_standard(self):
        """ The accounting impact of this cost method change is not neutral as we will use the last
        fifo price as the new standard price.
        """
        self.product = self.product_fifo

        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'standard'
        self.assertEqual(self.product.total_value, 290.0)
        self.assertEqual(self.product.qty_available, 19)

    def test_fifo_to_avco(self):
        """ The accounting impact of this cost method change is not neutral as we will use the last
        fifo price as the new AVCO.
        """
        self.product = self.product_fifo

        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'average'
        self.assertEqual(self.product.total_value, 285)
        self.assertEqual(self.product.qty_available, 19)

    def test_avco_to_standard(self):
        """ The accounting impact of this cost method change is neutral.
        """
        self.product = self.product_avco

        self._make_in_move(self.product, 10, unit_cost=10)
        self._make_in_move(self.product, 10, unit_cost=20)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'standard'
        self.assertEqual(self.product.total_value, 285)
        self.assertEqual(self.product.qty_available, 19)

    def test_standard_to_avco(self):
        """ The accounting impact of this cost method change is neutral.
        """
        self.product = self.product_standard
        self.product.product_tmpl_id.standard_price = 10

        self._make_in_move(self.product, 10)
        self._make_in_move(self.product, 10)
        self._make_out_move(self.product, 1)

        self.product.product_tmpl_id.categ_id.property_cost_method = 'average'
        self.assertEqual(self.product.total_value, 190)
        self.assertEqual(self.product.qty_available, 19)


@tagged('post_install', '-at_install', 'change_valuation')
class TestStockValuationChangeValuation(TestStockValuationCommon):
    @classmethod
    def setUpClass(cls):
        super(TestStockValuationChangeValuation, cls).setUpClass()
        cls.product = cls.product_standard

    def test_standard_manual_to_auto_1(self):
        self.product.product_tmpl_id.standard_price = 10
        self._make_in_move(self.product, 10)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        self.product.product_tmpl_id.categ_id.write({
            'property_valuation': 'real_time',
            'property_stock_valuation_account_id': self.account_stock_valuation.id,
        })

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        account_move_line = self.env['account.move'].browse(self.env.company.action_close_stock_valuation()['res_id']).line_ids
        self.assertEqual(len(account_move_line), 2)

    def test_standard_manual_to_auto_2(self):
        self.product.product_tmpl_id.standard_price = 10
        self._make_in_move(self.product, 10)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        # Try to change the product category with a `default_type` key in the context and
        # check it doesn't break the account move generation.
        self.product.with_context(default_is_storable=True).categ_id = self.category_standard_auto
        self.assertEqual(self.product.categ_id, self.category_standard_auto)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        account_move_line = self.env['account.move'].browse(self.env.company.action_close_stock_valuation()['res_id']).line_ids
        self.assertEqual(len(account_move_line), 2)

    def test_standard_auto_to_manual_1(self):
        self.product = self.product_standard_auto
        self.product.product_tmpl_id.standard_price = 10
        self._make_in_move(self.product, 10)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        self.product.product_tmpl_id.categ_id.property_valuation = 'periodic'

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        # An accounting entry should only be created for the emptying now that the category is manual.
        account_move_line = self.env['account.move'].browse(self.env.company.action_close_stock_valuation()['res_id']).line_ids
        self.assertEqual(len(account_move_line), 2)

    def test_standard_auto_to_manual_2(self):
        self.product = self.product_standard_auto
        self.product.product_tmpl_id.standard_price = 10
        self._make_in_move(self.product, 10)

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        self.product.with_context(debug=True).categ_id = self.category_standard

        self.assertEqual(self.product.total_value, 100)
        self.assertEqual(self.product.qty_available, 10)

        # account_move_line = self.env['account.move'].browse(self.env.company.action_close_stock_valuation()['res_id']).line_ids
        # self.assertEqual(len(account_move_line), 2)

    def test_return_delivery_fifo(self):
        self.product = self.product_fifo
        self.env['decimal.precision'].search([
            ('name', '=', 'Product Price'),
        ]).digits = 4
        self.product.standard_price = 280.8475

        move1 = self._make_out_move(self.product, 4, create_picking=True, force_assign=True)
        move2 = self._make_return(move1, 4)

        for move in [move1, move2]:
            self.assertAlmostEqual(move._get_price_unit(), self.product.standard_price)
            self.assertAlmostEqual(abs(move.value), 1123.39)


class TestAngloSaxonAccounting(TestStockValuationCommon):
    def test_avco_and_credit_note(self):
        """
        When reversing an invoice that contains some anglo-saxo AML, the new anglo-saxo AML should have the same value
        """
        # Required for `account_id` to be visible in the view
        self.env.user.group_ids += self.env.ref('account.group_account_readonly')
        self.product = self.product_avco_auto

        self._make_in_move(self.product, 2, unit_cost=10)

        invoice = self._create_invoice(self.product, 2, 25)

        self._make_in_move(self.product, 2, unit_cost=20)
        # self.assertEqual(self.product.standard_price, 15)

        refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice.ids).create({
            'journal_id': invoice.journal_id.id,
        })
        action = refund_wizard.refund_moves()
        reverse_invoice = self.env['account.move'].browse(action['res_id'])
        with Form(reverse_invoice) as reverse_invoice_form:
            with reverse_invoice_form.invoice_line_ids.edit(0) as line:
                line.quantity = 1
        reverse_invoice.action_post()

        anglo_lines = reverse_invoice.line_ids.filtered(lambda l: l.display_type == 'cogs')
        self.assertEqual(len(anglo_lines), 2)
        self.assertEqual(abs(anglo_lines[0].balance), 10)
        self.assertEqual(abs(anglo_lines[1].balance), 10)

    def test_return_delivery_storno(self):
        """ When using STORNO accounting, reverse accounting moves should have negative values for credit/debit.
        """
        self.env.company.account_storno = True
        self.product = self.product_fifo

        self._make_in_move(self.product, 10, unit_cost=10)
        out_move = self._make_out_move(self.product, 10, create_picking=True)
        self._make_return(out_move, 10)

        out_invoice = self._create_invoice(self.product, 10, 10)
        return_credit_note = self._create_credit_note(self.product, 10, 10, reversed_entry_id=out_invoice.id)

        out_move_line_ids = out_invoice.line_ids

        self.assertEqual(out_move_line_ids[0].credit, 100)
        self.assertEqual(out_move_line_ids[0].debit, 0)
        self.assertEqual(out_move_line_ids[1].credit, 0)
        self.assertEqual(out_move_line_ids[1].debit, 100)

        return_line_ids = return_credit_note.line_ids

        self.assertEqual(return_line_ids[0].credit, -100)
        self.assertEqual(return_line_ids[0].debit, 0)
        self.assertEqual(return_line_ids[1].credit, 0)
        self.assertEqual(return_line_ids[1].debit, -100)
