mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-25 13:02:07 +02:00
970 lines
40 KiB
Python
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)
|