[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:
Simon Lejeune
2019-06-09 14:11:36 +00:00
parent 6127608cae
commit 168f4d75ef
5 changed files with 151 additions and 27 deletions
+2
View File
@@ -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):
+18 -10
View File
@@ -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)