[REF] stock_account: average cost method with valuation layers
One of the goal of this refactoring is also to run the vacuum when the cost method is AVCO. This will be used allow already out landed cost on AVCO and also to have a consistent value when going into negative. We update `_prepare_in_svl_vals` to set the remaining_qty field that will be used when fifo is implemented. On the stock move, we create the layer in with the real price unit of the move and adapt product_price_update_before_done. This method will now compute the AVCO according to the quantity valued in stock (quantity_svl) instead of all the quantities in stock (qty_available), which was wrong (qty_available could contain consigned quants). We also make sure to recompute the avco when editing a done move. We don't implement yet the negative handling as we'll need fifo for that. task-1875873
This commit is contained in:
@@ -98,6 +98,8 @@ class ProductProduct(models.Model):
|
||||
'unit_cost': unit_cost,
|
||||
'quantity': quantity,
|
||||
}
|
||||
if self.cost_method in ('average'):
|
||||
vals['remaining_qty'] = quantity
|
||||
return vals
|
||||
|
||||
def _prepare_out_svl_vals(self, quantity):
|
||||
|
||||
@@ -211,7 +211,9 @@ class StockMove(models.Model):
|
||||
valued_quantity = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id)
|
||||
unit_cost = move.product_id.standard_price
|
||||
unit_cost = abs(move._get_price_unit()) # May be negative (i.e. decrease an out move).
|
||||
if move.product_id.cost_method == 'standard':
|
||||
unit_cost = move.product_id.standard_price
|
||||
svl_vals = move.product_id._prepare_in_svl_vals(forced_quantity or valued_quantity, unit_cost)
|
||||
svl_vals.update(move._prepare_common_svl_vals())
|
||||
if forced_quantity:
|
||||
@@ -374,9 +376,6 @@ class StockMove(models.Model):
|
||||
return value_to_return
|
||||
|
||||
def _action_done(self, cancel_backorder=False):
|
||||
self.product_price_update_before_done()
|
||||
res = super(StockMove, self)._action_done(cancel_backorder=cancel_backorder)
|
||||
|
||||
# Init a dict that will group the moves by valuation type, according to `move._is_valued_type`.
|
||||
valued_moves = {valued_type: self.env['stock.move'] for valued_type in self._get_valued_types()}
|
||||
for move in self:
|
||||
@@ -385,14 +384,19 @@ class StockMove(models.Model):
|
||||
valued_moves[valued_type] |= move
|
||||
continue
|
||||
|
||||
# AVCO application
|
||||
valued_moves['in'].product_price_update_before_done()
|
||||
|
||||
res = super(StockMove, self)._action_done(cancel_backorder=cancel_backorder)
|
||||
|
||||
valued_moves_set = self.env['stock.move']
|
||||
standard_valued_moves_set = self.env['stock.move']
|
||||
standard_average_valued_moves_set = self.env['stock.move']
|
||||
for valued_type, valued_move_list in valued_moves.items():
|
||||
for valued_move in valued_move_list:
|
||||
if valued_move._is_dropshipped() or valued_move._is_dropshipped_returned():
|
||||
continue
|
||||
if valued_move.product_id.cost_method == 'standard':
|
||||
standard_valued_moves_set |= valued_move
|
||||
if valued_move.product_id.cost_method in ('standard', 'average'):
|
||||
standard_average_valued_moves_set |= valued_move
|
||||
else:
|
||||
valued_moves_set |= valued_move
|
||||
|
||||
@@ -400,7 +404,7 @@ class StockMove(models.Model):
|
||||
# Create the valuation layers in batch by calling `moves._create_valued_type_svl`.
|
||||
for valued_type in self._get_valued_types():
|
||||
todo_valued_moves = valued_moves[valued_type]
|
||||
if todo_valued_moves and todo_valued_moves in standard_valued_moves_set:
|
||||
if todo_valued_moves and todo_valued_moves <= standard_average_valued_moves_set:
|
||||
stock_valuation_layers |= getattr(todo_valued_moves, '_create_%s_svl' % valued_type)()
|
||||
continue
|
||||
|
||||
@@ -436,10 +440,14 @@ class StockMove(models.Model):
|
||||
# adapt standard price on incomming moves if the product cost_method is 'average'
|
||||
std_price_update = {}
|
||||
for move in self.filtered(lambda move: move._is_in() and move.product_id.cost_method == 'average'):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
product_tot_qty_available = move.product_id.quantity_svl + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
valued_move_lines = move._get_in_move_lines()
|
||||
qty_done = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
qty_done += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id)
|
||||
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
|
||||
|
||||
@@ -105,6 +105,7 @@ class StockMoveLine(models.Model):
|
||||
@api.model
|
||||
def _create_correction_svl(self, move, diff):
|
||||
if move._is_in() and diff > 0 or move._is_out() and diff < 0:
|
||||
move.product_price_update_before_done(forced_qty=diff)
|
||||
move._create_in_svl(forced_quantity=abs(diff))
|
||||
elif move._is_in() and diff < 0 or move._is_out() and diff > 0:
|
||||
move._create_out_svl(forced_quantity=abs(diff))
|
||||
|
||||
@@ -2325,7 +2325,7 @@ class TestStockValuation(SavepointCase):
|
||||
move1.move_line_ids.qty_done = 60.0
|
||||
move1._action_done()
|
||||
|
||||
self.assertEqual(move1.value, 900.0)
|
||||
self.assertEqual(move1.stock_valuation_layer_ids.value, 900.0)
|
||||
|
||||
# Purchase 140 units @ 15.50 per unit
|
||||
move2 = self.env['stock.move'].create({
|
||||
@@ -2342,7 +2342,7 @@ class TestStockValuation(SavepointCase):
|
||||
move2.move_line_ids.qty_done = 140.0
|
||||
move2._action_done()
|
||||
|
||||
self.assertEqual(move2.value, 2170.0)
|
||||
self.assertEqual(move2.stock_valuation_layer_ids.value, 2170.0)
|
||||
|
||||
# Sale 190 units @ 15.35 per unit
|
||||
move3 = self.env['stock.move'].create({
|
||||
@@ -2358,8 +2358,7 @@ class TestStockValuation(SavepointCase):
|
||||
move3.move_line_ids.qty_done = 190.0
|
||||
move3._action_done()
|
||||
|
||||
|
||||
self.assertEqual(move3.value, -2916.5)
|
||||
self.assertEqual(move3.stock_valuation_layer_ids.value, -2916.5)
|
||||
|
||||
# Purchase 70 units @ $16.00 per unit
|
||||
move4 = self.env['stock.move'].create({
|
||||
@@ -2376,7 +2375,7 @@ class TestStockValuation(SavepointCase):
|
||||
move4.move_line_ids.qty_done = 70.0
|
||||
move4._action_done()
|
||||
|
||||
self.assertEqual(move4.value, 1120.0)
|
||||
self.assertEqual(move4.stock_valuation_layer_ids.value, 1120.0)
|
||||
|
||||
# Sale 30 units @ $19.50 per unit
|
||||
move5 = self.env['stock.move'].create({
|
||||
@@ -2392,7 +2391,7 @@ class TestStockValuation(SavepointCase):
|
||||
move5.move_line_ids.qty_done = 30.0
|
||||
move5._action_done()
|
||||
|
||||
self.assertEqual(move5.value, -477.6)
|
||||
self.assertEqual(move5.stock_valuation_layer_ids.value, -477.6)
|
||||
|
||||
# Receives 10 units but assign them to an owner, the valuation should not be impacted.
|
||||
move6 = self.env['stock.move'].create({
|
||||
@@ -2410,8 +2409,9 @@ class TestStockValuation(SavepointCase):
|
||||
move6.move_line_ids.qty_done = 10.0
|
||||
move6._action_done()
|
||||
|
||||
self.assertEqual(move6.value, 0)
|
||||
self.assertEqual(move6.stock_valuation_layer_ids.value, 0)
|
||||
|
||||
@skip('it is actually an average negative test')
|
||||
def test_average_perpetual_2(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
|
||||
@@ -2473,6 +2473,7 @@ class TestStockValuation(SavepointCase):
|
||||
|
||||
self.assertEqual(self.product1.stock_value, 87.5)
|
||||
|
||||
@skip('it is actually an average negative test')
|
||||
def test_average_perpetual_3(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
|
||||
@@ -2533,6 +2534,7 @@ class TestStockValuation(SavepointCase):
|
||||
self.assertEqual(self.product1.stock_value, -187.5)
|
||||
|
||||
def test_average_perpetual_4(self):
|
||||
"""receive 1@10, receive 1@5 insteadof 3@5"""
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
@@ -2563,8 +2565,7 @@ class TestStockValuation(SavepointCase):
|
||||
move2.move_line_ids.qty_done = 1.0
|
||||
move2._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.qty_available, 2.0)
|
||||
self.assertAlmostEqual(self.product1.qty_at_date, 2.0)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 2.0)
|
||||
self.assertAlmostEqual(self.product1.standard_price, 7.5)
|
||||
|
||||
def test_average_perpetual_5(self):
|
||||
@@ -2586,12 +2587,12 @@ class TestStockValuation(SavepointCase):
|
||||
move1.move_line_ids.owner_id = self.owner1.id
|
||||
move1._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.qty_available, 1.0)
|
||||
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
|
||||
self.assertAlmostEqual(self.product1.stock_value, 0.0)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 0.0)
|
||||
|
||||
def test_average_perpetual_6(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
""" Batch validation of moves """
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'Receive 1 unit at 10',
|
||||
@@ -2623,6 +2624,8 @@ class TestStockValuation(SavepointCase):
|
||||
(move1 | move2)._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 7.5)
|
||||
self.assertEqual(self.product1.quantity_svl, 2)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
|
||||
def test_average_perpetual_7(self):
|
||||
""" Test edit in the past. Receive 5@10, receive 10@20, edit the first move to receive
|
||||
@@ -2644,6 +2647,9 @@ class TestStockValuation(SavepointCase):
|
||||
move1._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 10)
|
||||
self.assertAlmostEqual(move1.stock_valuation_layer_ids.value, 50)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 5)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 50)
|
||||
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'IN 10@20',
|
||||
@@ -2659,10 +2665,17 @@ class TestStockValuation(SavepointCase):
|
||||
move2._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 16.67)
|
||||
self.assertAlmostEqual(move2.stock_valuation_layer_ids.value, 200)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 15)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 250)
|
||||
|
||||
move1.move_line_ids.qty_done = 15
|
||||
move1.move_line_ids.with_context(svl=True).qty_done = 15
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 14.0)
|
||||
self.assertAlmostEqual(len(move1.stock_valuation_layer_ids), 2)
|
||||
self.assertAlmostEqual(move1.stock_valuation_layer_ids[-1].value, 100)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 25)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 350)
|
||||
|
||||
def test_average_perpetual_8(self):
|
||||
""" Receive 1@10, then dropship 1@20, finally return the dropship. Dropship should not
|
||||
@@ -2715,6 +2728,7 @@ class TestStockValuation(SavepointCase):
|
||||
|
||||
self.assertAlmostEqual(self.product1.standard_price, 10.0)
|
||||
|
||||
@skip('avco negative test')
|
||||
def test_average_negative_1(self):
|
||||
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
|
||||
"""
|
||||
@@ -2769,6 +2783,7 @@ class TestStockValuation(SavepointCase):
|
||||
self.assertEqual(move2_valuation_aml.debit, 0)
|
||||
self.assertEqual(move2_valuation_aml.credit, 10)
|
||||
|
||||
@skip('avco negative test')
|
||||
def test_average_negative_2(self):
|
||||
""" Send goods that you don't have in stock and never received any unit.
|
||||
"""
|
||||
@@ -2792,6 +2807,7 @@ class TestStockValuation(SavepointCase):
|
||||
move1._action_done()
|
||||
self.assertEqual(move1.value, -990.0) # as no move out were done for this product, fallback on the standard price
|
||||
|
||||
@skip('avco negative test')
|
||||
def test_average_negative_3(self):
|
||||
""" Send goods that you don't have in stock but received and send some units before.
|
||||
"""
|
||||
@@ -2849,6 +2865,7 @@ class TestStockValuation(SavepointCase):
|
||||
|
||||
self.assertEqual(move3.value, -100.0) # as no move out were done for this product, fallback on latest cost
|
||||
|
||||
@skip('avco negative test')
|
||||
def test_average_negative_4(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
|
||||
@@ -2872,6 +2889,7 @@ class TestStockValuation(SavepointCase):
|
||||
|
||||
self.assertEqual(move1.value, 100.0)
|
||||
|
||||
@skip('avco negative test')
|
||||
def test_average_negative_5(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
|
||||
@@ -3017,9 +3035,8 @@ class TestStockValuation(SavepointCase):
|
||||
move1.move_line_ids.owner_id = self.owner1.id
|
||||
move1._action_done()
|
||||
|
||||
self.assertAlmostEqual(self.product1.qty_available, 1.0)
|
||||
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
|
||||
self.assertAlmostEqual(self.product1.stock_value, 0.0)
|
||||
self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
|
||||
self.assertAlmostEqual(self.product1.value_svl, 0.0)
|
||||
|
||||
def test_standard_perpetual_1(self):
|
||||
''' Set owner on incoming move => no valuation '''
|
||||
|
||||
@@ -200,3 +200,99 @@ class TestStockValuationStandard(TestStockValuationCommon):
|
||||
self.assertEqual(self.product1.quantity_svl, 5)
|
||||
self.assertEqual(self.product1.stock_valuation_layer_ids[-1].description, 'Product value manually modified (from 10.0 to 15.0)')
|
||||
|
||||
|
||||
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'].with_context(svl=True).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'].with_context(svl=True).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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user