summaryrefslogtreecommitdiff
path: root/addons/stock_account/tests/test_stockvaluationlayer.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/stock_account/tests/test_stockvaluationlayer.py')
-rw-r--r--addons/stock_account/tests/test_stockvaluationlayer.py900
1 files changed, 900 insertions, 0 deletions
diff --git a/addons/stock_account/tests/test_stockvaluationlayer.py b/addons/stock_account/tests/test_stockvaluationlayer.py
new file mode 100644
index 00000000..136bc967
--- /dev/null
+++ b/addons/stock_account/tests/test_stockvaluationlayer.py
@@ -0,0 +1,900 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """
+
+from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
+from odoo.tests import Form, tagged
+from odoo.tests.common import SavepointCase, TransactionCase
+
+
+class TestStockValuationCommon(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuationCommon, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.picking_type_in = cls.env.ref('stock.picking_type_in')
+ cls.picking_type_out = cls.env.ref('stock.picking_type_out')
+
+ def setUp(self):
+ super(TestStockValuationCommon, self).setUp()
+ # Counter automatically incremented by `_make_in_move` and `_make_out_move`.
+ self.days = 0
+
+ def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False):
+ """ Helper to create and validate a receipt move.
+ """
+ unit_cost = unit_cost or product.standard_price
+ in_move = self.env['stock.move'].create({
+ 'name': 'in %s units @ %s per unit' % (str(quantity), str(unit_cost)),
+ 'product_id': product.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'price_unit': unit_cost,
+ 'picking_type_id': self.picking_type_in.id,
+ })
+
+ if create_picking:
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': in_move.picking_type_id.id,
+ 'location_id': in_move.location_id.id,
+ 'location_dest_id': in_move.location_dest_id.id,
+ })
+ in_move.write({'picking_id': picking.id})
+
+ in_move._action_confirm()
+ in_move._action_assign()
+ in_move.move_line_ids.qty_done = quantity
+ in_move._action_done()
+
+ self.days += 1
+ return in_move.with_context(svl=True)
+
+ def _make_out_move(self, product, quantity, force_assign=None, create_picking=False):
+ """ Helper to create and validate a delivery move.
+ """
+ out_move = self.env['stock.move'].create({
+ 'name': 'out %s units' % str(quantity),
+ 'product_id': product.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'picking_type_id': self.picking_type_out.id,
+ })
+
+ if create_picking:
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': out_move.picking_type_id.id,
+ 'location_id': out_move.location_id.id,
+ 'location_dest_id': out_move.location_dest_id.id,
+ })
+ out_move.write({'picking_id': picking.id})
+
+ out_move._action_confirm()
+ out_move._action_assign()
+ if force_assign:
+ self.env['stock.move.line'].create({
+ 'move_id': out_move.id,
+ 'product_id': out_move.product_id.id,
+ 'product_uom_id': out_move.product_uom.id,
+ 'location_id': out_move.location_id.id,
+ 'location_dest_id': out_move.location_dest_id.id,
+ })
+ out_move.move_line_ids.qty_done = quantity
+ out_move._action_done()
+
+ self.days += 1
+ return out_move.with_context(svl=True)
+
+ def _make_dropship_move(self, product, quantity, unit_cost=None):
+ dropshipped = self.env['stock.move'].create({
+ 'name': 'dropship %s units' % str(quantity),
+ 'product_id': product.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'picking_type_id': self.picking_type_out.id,
+ })
+ if unit_cost:
+ dropshipped.price_unit = unit_cost
+ dropshipped._action_confirm()
+ dropshipped._action_assign()
+ dropshipped.move_line_ids.qty_done = quantity
+ dropshipped._action_done()
+ return dropshipped
+
+ def _make_return(self, move, quantity_to_return):
+ stock_return_picking = Form(self.env['stock.return.picking']\
+ .with_context(active_ids=[move.picking_id.id], active_id=move.picking_id.id, active_model='stock.picking'))
+ stock_return_picking = stock_return_picking.save()
+ stock_return_picking.product_return_moves.quantity = quantity_to_return
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].qty_done = quantity_to_return
+ return_pick._action_done()
+ return return_pick.move_lines
+
+
+class TestStockValuationStandard(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationStandard, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+
+ self.assertEqual(self.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_increase_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 15
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_add_ml_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 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.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 1)
+ move2.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_decrease_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 5)
+ move2.move_line_ids.qty_done = 1
+
+ self.assertEqual(self.product1.value_svl, 90)
+ self.assertEqual(self.product1.quantity_svl, 9)
+
+ def test_change_standard_price_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+
+ # change cost from 10 to 15
+ self.product1.standard_price = 15.0
+
+ self.assertEqual(self.product1.value_svl, 75)
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(self.product1.stock_valuation_layer_ids.sorted()[-1].description, 'Product value manually modified (from 10.0 to 15.0)')
+
+ def test_negative_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 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.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_dropship_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_dropship_move(self.product1, 10)
+
+ valuation_layers = self.product1.stock_valuation_layer_ids
+ self.assertEqual(len(valuation_layers), 2)
+ self.assertEqual(valuation_layers[0].value, 100)
+ self.assertEqual(valuation_layers[1].value, -100)
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_increase_dropship_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_dropship_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 15
+
+ valuation_layers = self.product1.stock_valuation_layer_ids
+ self.assertEqual(len(valuation_layers), 4)
+ self.assertEqual(valuation_layers[0].value, 100)
+ self.assertEqual(valuation_layers[1].value, -100)
+ self.assertEqual(valuation_layers[2].value, 50)
+ self.assertEqual(valuation_layers[3].value, -50)
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_empty_stock_move_valorisation(self):
+ product1 = self.env['product.product'].create({
+ 'name': 'p1',
+ 'type': 'product',
+ })
+ product2 = self.env['product.product'].create({
+ 'name': 'p2',
+ 'type': 'product',
+ })
+ 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({
+ 'name': 'in %s units @ %s per unit' % (2, str(10)),
+ 'product_id': product.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom': self.uom_unit.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.qty_done = 2
+ 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'))
+ res_dict_for_back_order = wizard.process()
+
+ self.assertTrue(product2.stock_valuation_layer_ids)
+ self.assertFalse(product1.stock_valuation_layer_ids)
+
+
+class TestStockValuationAVCO(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationAVCO, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ self.assertEqual(self.product1.standard_price, 10)
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 200)
+ move3 = self._make_out_move(self.product1, 15)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -225)
+
+ self.assertEqual(self.product1.value_svl, 75)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_increase_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 15
+
+ self.assertEqual(self.product1.value_svl, 125)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_add_ml_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 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.product1.value_svl, 125)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 12.5)
+
+ def test_change_in_past_add_move_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'product_id': move1.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'state': 'done',
+ 'picking_id': move1.picking_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_change_in_past_increase_out_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move3.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_change_in_past_decrease_out_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move3.move_line_ids.qty_done = 10
+
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 10)
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_negative_1(self):
+ """ Ensures that, in AVCO, the `remaining_qty` field is computed and the vacuum is ran
+ when necessary.
+ """
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 30)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
+ move4 = self._make_in_move(self.product1, 10, unit_cost=30)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+ move5 = self._make_in_move(self.product1, 10, unit_cost=40)
+
+ self.assertEqual(self.product1.value_svl, 400)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_negative_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.standard_price = 10
+ move1 = self._make_out_move(self.product1, 1, force_assign=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=15)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_negative_3(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_out_move(self.product1, 2, force_assign=True)
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 0)
+ move2 = self._make_in_move(self.product1, 20, unit_cost=3.33)
+ self.assertEqual(move1.stock_valuation_layer_ids[1].value, -6.66)
+
+ self.assertEqual(self.product1.standard_price, 3.33)
+ self.assertEqual(self.product1.value_svl, 59.94)
+ self.assertEqual(self.product1.quantity_svl, 18)
+
+ def test_return_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_return_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 2)
+
+ def test_rereturn_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1) # -15, current avco
+ move5 = self._make_return(move4, 1) # +10, original move's price unit
+
+ self.assertEqual(self.product1.value_svl, 15)
+ self.assertEqual(self.product1.quantity_svl, 1)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
+
+ def test_rereturn_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 15)
+ self.assertEqual(self.product1.quantity_svl, 1)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
+
+ def test_dropship_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_rounding_slv_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=1.00)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=1.00)
+ move3 = self._make_in_move(self.product1, 1, unit_cost=1.01)
+
+ self.assertAlmostEqual(self.product1.value_svl, 3.01)
+
+ move_out = self._make_out_move(self.product1, 3, create_picking=True)
+
+ self.assertIn('Rounding Adjustment: -0.01', move_out.stock_valuation_layer_ids.description)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 1.00)
+
+ def test_rounding_slv_2(self):
+ self._make_in_move(self.product1, 1, unit_cost=1.02)
+ self._make_in_move(self.product1, 1, unit_cost=1.00)
+ self._make_in_move(self.product1, 1, unit_cost=1.00)
+
+ self.assertAlmostEqual(self.product1.value_svl, 3.02)
+
+ move_out = self._make_out_move(self.product1, 3, create_picking=True)
+
+ self.assertIn('Rounding Adjustment: +0.01', move_out.stock_valuation_layer_ids.description)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 1.01)
+
+
+class TestStockValuationFIFO(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationFIFO, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 5)
+
+ def test_negative_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 30)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
+ move4 = self._make_in_move(self.product1, 10, unit_cost=30)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+ move5 = self._make_in_move(self.product1, 10, unit_cost=40)
+
+ self.assertEqual(self.product1.value_svl, 400)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 10
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_decrease_in_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 10
+ move4 = self._make_in_move(self.product1, 20, unit_cost=15)
+
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=15)
+ move3 = self._make_out_move(self.product1, 20)
+ move1.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_in_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=12)
+ move3 = self._make_out_move(self.product1, 15)
+ move4 = self._make_out_move(self.product1, 20)
+ move5 = self._make_in_move(self.product1, 100, unit_cost=15)
+ move1.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 1375)
+ self.assertEqual(self.product1.quantity_svl, 95)
+
+ def test_change_in_past_increase_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ move2.move_line_ids.qty_done = 25
+
+ self.assertEqual(self.product1.value_svl, 225)
+ self.assertEqual(self.product1.quantity_svl, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 15)
+
+ def test_change_in_past_decrease_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 15)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ move2.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 450)
+ self.assertEqual(self.product1.quantity_svl, 35)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 35)
+
+ def test_change_in_past_add_ml_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ self.env['stock.move.line'].create({
+ 'move_id': move2.id,
+ 'product_id': move2.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move2.product_uom.id,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 350)
+ self.assertEqual(self.product1.quantity_svl, 25)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 25)
+
+ def test_return_delivery_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10, create_picking=True)
+ move3 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move4 = self._make_return(move2, 10)
+
+ self.assertEqual(self.product1.value_svl, 300)
+ self.assertEqual(self.product1.quantity_svl, 20)
+
+ def test_return_receipt_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_return(move1, 2)
+
+ self.assertEqual(self.product1.value_svl, 280)
+ self.assertEqual(self.product1.quantity_svl, 18)
+
+ def test_rereturn_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 20)
+ self.assertEqual(self.product1.quantity_svl, 1)
+
+ def test_rereturn_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 10)
+ self.assertEqual(self.product1.quantity_svl, 1)
+
+ def test_dropship_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertAlmostEqual(self.product1.standard_price, 10)
+
+
+class TestStockValuationChangeCostMethod(TestStockValuationCommon):
+ def test_standard_to_fifo_1(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
+ for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
+ self.assertEqual(svl.description, 'Costing method change for product category All: from standard to fifo.')
+
+ 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.product1` to another one, not changing the current one.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ cat2 = self.env['product.category'].create({'name': 'fifo'})
+ cat2.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id = cat2
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
+
+ def test_avco_to_fifo(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.assertEqual(self.product1.value_svl, 285)
+ self.assertEqual(self.product1.quantity_svl, 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.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.assertEqual(self.product1.value_svl, 380)
+ self.assertEqual(self.product1.quantity_svl, 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.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.assertEqual(self.product1.value_svl, 380)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_avco_to_standard(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.assertEqual(self.product1.value_svl, 285)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_standard_to_avco(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+
+class TestStockValuationChangeValuation(TestStockValuationCommon):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuationChangeValuation, cls).setUpClass()
+ cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
+ cls.product1.categ_id.property_valuation = 'real_time'
+ cls.product1.write({
+ 'property_account_expense_id': cls.expense_account.id,
+ })
+ cls.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': cls.stock_input_account.id,
+ 'property_stock_account_output_categ_id': cls.stock_output_account.id,
+ 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
+ 'property_stock_journal': cls.stock_journal.id,
+ })
+
+ def test_standard_manual_to_auto_1(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the replenish now that the category is perpetual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+ for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
+ self.assertEqual(svl.description, 'Valuation method change for product category All: from manual_periodic to real_time.')
+
+ def test_standard_manual_to_auto_2(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ cat2 = self.env['product.category'].create({'name': 'standard auto'})
+ cat2.property_cost_method = 'standard'
+ cat2.property_valuation = 'real_time'
+ cat2.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+
+ # 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.product1.with_context(default_type='product').categ_id = cat2
+ self.assertEqual(self.product1.categ_id, cat2)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the replenish now that the category is perpetual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+
+ def test_standard_auto_to_manual_1(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the emptying now that the category is manual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+
+ def test_standard_auto_to_manual_2(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ cat2 = self.env['product.category'].create({'name': 'fifo'})
+ cat2.property_cost_method = 'standard'
+ cat2.property_valuation = 'manual_periodic'
+ self.product1.with_context(debug=True).categ_id = cat2
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the emptying now that the category is manual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+