oca-ocb-accounting/odoo-bringout-oca-ocb-stock_account/stock_account/tests/test_stockvaluationlayer.py
Ernad Husremovic 768b70e05e 19.0 vanilla
2026-03-09 09:30:07 +01:00

970 lines
40 KiB
Python

""" Implementation of "INVENTORY VALUATION TESTS" spreadsheet. """
from odoo import Command
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)
def test_return_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)
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)
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)