[FIX] stock, stock_account: valuation with owner

Issue 1
-------

- Set a product with:
  Inventory Valuation: Manual
  Costing Method: Average Cost (or Standard Price)
- Receive 1 unit @ 10, set an owner

In the 'Inventory Valuation' report, the product is valued @ 10 while no
accounting entry was created.

The system simply multiplies the available quantity by the cost price,
without taking into account the ownership of the quants.

Issue 2
-------

- Set a product with:
  Inventory Valuation: Automated
  Costing Method: Average Cost (or Standard Price)
- Receive 1 unit @ 10, set an owner

In the 'Inventory Valuation' report, the product is valued @ 10 while no
accounting entry was created.

Once again, the system simply multiplies the available quantity by the
cost price, without taking into account the AML.

Solution
--------

A slight refactoring of the `_compute_stock_value` method is necessary
to make consistent the the FIFO computation and the Standard / Average
method:
- in case of manual valuation, the quantity on stock moves are taken
  into account
- in case of perpetual valuation, the quantity on AMLs is taken into
  account

The method `_compute_quantities_dict` needs a minor change so a `False`
value can be handled correctly.

The FIFO method is slightly changed:
- the quantity at date takes into account the owner
- in case of perpetual valuation, AMLs are used instead of stock moves

Bonus point: an error was discovered in `_create_account_move_line`
thanks to the test `test_change_cost_method_1`. The forced quantity
should follow the same sign logic than the product quantity.

opw-1849535
This commit is contained in:
Nicolas Martinelli
2018-06-07 08:30:04 +02:00
committed by Nicolas Martinelli
parent ab1690b3de
commit 21099f3eaf
4 changed files with 190 additions and 34 deletions
+3 -3
View File
@@ -98,13 +98,13 @@ class Product(models.Model):
domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc
domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc
if lot_id:
if lot_id is not None:
domain_quant += [('lot_id', '=', lot_id)]
if owner_id:
if owner_id is not None:
domain_quant += [('owner_id', '=', owner_id)]
domain_move_in += [('restrict_partner_id', '=', owner_id)]
domain_move_out += [('restrict_partner_id', '=', owner_id)]
if package_id:
if package_id is not None:
domain_quant += [('package_id', '=', package_id)]
if dates_in_the_past:
domain_move_in_done = list(domain_move_in)
+34 -29
View File
@@ -198,39 +198,45 @@ class ProductProduct(models.Model):
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
for product in self:
if product.cost_method in ['standard', 'average']:
qty_available = product.with_context(company_owned=True).qty_available
price_used = product.standard_price
if to_date:
price_used = product.get_history_price(
self.env.user.company_id.id,
date=to_date,
)
product.stock_value = price_used * qty_available
product.qty_at_date = qty_available
elif product.cost_method == 'fifo':
if to_date:
if product.product_tmpl_id.valuation == 'manual_periodic':
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
moves = StockMove.search(domain)
if to_date:
price_used = product.get_history_price(
self.env.user.company_id.id,
date=to_date,
) if product.cost_method in ['standard', 'average'] else 0.0
if product.product_tmpl_id.valuation == 'manual_periodic':
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
moves = StockMove.search(domain)
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.cost_method == 'fifo':
product.stock_value = sum(moves.mapped('value'))
product.qty_at_date = product.with_context(company_owned=True).qty_available
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
elif product.cost_method in ['standard', 'average']:
product.stock_value = product.qty_at_date * price_used
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
product.qty_at_date = quantity
if product.cost_method == 'fifo':
product.stock_value = value
product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
else:
product.stock_value, moves = product._sum_remaining_values()
product.qty_at_date = product.with_context(company_owned=True).qty_available
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_fifo_manual_move_ids = moves
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
elif product.cost_method in ['standard', 'average']:
product.stock_value = quantity * price_used
else:
if product.product_tmpl_id.valuation == 'manual_periodic':
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.cost_method == 'fifo':
product.stock_value, product.stock_fifo_manual_move_ids = product._sum_remaining_values()
elif product.cost_method in ['standard', 'average']:
product.stock_value = product.qty_at_date * product.standard_price
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
product.qty_at_date = quantity
if product.cost_method == 'fifo':
product.stock_value = value
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
elif product.cost_method in ['standard', 'average']:
product.stock_value = quantity * product.standard_price
def action_valuation_at_date_details(self):
""" Returns an action with either a list view of all the valued stock moves of `self` if the
@@ -401,4 +407,3 @@ class ProductCategory(models.Model):
'message': _("Changing your cost method is an important change that will impact your inventory valuation. Are you sure you want to make that change?"),
}
}
+2 -1
View File
@@ -581,7 +581,8 @@ class StockMove(models.Model):
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id):
self.ensure_one()
AccountMove = self.env['account.move']
quantity = self.env.context.get('forced_quantity', self.product_qty if self._is_in() else -1 * self.product_qty)
quantity = self.env.context.get('forced_quantity', self.product_qty)
quantity = quantity if self._is_in() else -1 * quantity
# Make an informative `ref` on the created account move to differentiate between classic
# movements, vacuum and edition of past moves.
@@ -2046,6 +2046,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move1.account_move_ids), 1)
self.assertAlmostEqual(self.product1.qty_available, 10.0)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# ---------------------------------------------------------------------
@@ -2088,6 +2090,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move2.account_move_ids), 1)
self.assertAlmostEqual(self.product1.qty_available, 20.0)
self.assertAlmostEqual(self.product1.qty_at_date, 20.0)
self.assertEqual(self.product1.stock_value, 220)
# ---------------------------------------------------------------------
@@ -2132,6 +2136,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move3.account_move_ids), 1)
self.assertAlmostEqual(self.product1.qty_available, 12.0)
self.assertAlmostEqual(self.product1.qty_at_date, 12.0)
self.assertEqual(self.product1.stock_value, 140)
# ---------------------------------------------------------------------
@@ -2165,6 +2171,7 @@ class TestStockValuation(TransactionCase):
# Ending
# ---------------------------------------------------------------------
self.assertEqual(self.product1.qty_available, 6)
self.assertAlmostEqual(self.product1.qty_at_date, 6.0)
self.assertEqual(self.product1.stock_value, 72)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 220)
@@ -2520,8 +2527,33 @@ class TestStockValuation(TransactionCase):
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.standard_price, 7.5)
def test_average_perpetual_5(self):
''' Set owner on incoming move => no valuation '''
self.product1.product_tmpl_id.cost_method = 'average'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
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)
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
@@ -2810,6 +2842,77 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move7.value, 100.0)
self.assertEqual(self.product1.standard_price, 10)
def test_average_manual_1(self):
''' Set owner on incoming move => no valuation '''
self.product1.product_tmpl_id.cost_method = 'average'
self.product1.product_tmpl_id.valuation = 'manual_periodic'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
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)
def test_standard_perpetual_1(self):
''' Set owner on incoming move => no valuation '''
self.product1.product_tmpl_id.cost_method = 'standard'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
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)
def test_standard_manual_1(self):
''' Set owner on incoming move => no valuation '''
self.product1.product_tmpl_id.cost_method = 'standard'
self.product1.product_tmpl_id.valuation = 'manual_periodic'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
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)
def test_change_cost_method_1(self):
""" Change the cost method from FIFO to AVCO.
"""
@@ -2862,6 +2965,8 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 1.0
move3._action_done()
self.assertAlmostEqual(self.product1.qty_available, 19)
self.assertAlmostEqual(self.product1.qty_at_date, 19)
self.assertEqual(self.product1.stock_value, 240)
# ---------------------------------------------------------------------
@@ -2930,6 +3035,8 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 1.0
move3._action_done()
self.assertAlmostEqual(self.product1.qty_available, 19)
self.assertAlmostEqual(self.product1.qty_at_date, 19)
self.assertEqual(self.product1.stock_value, 240)
# ---------------------------------------------------------------------
@@ -3003,6 +3110,8 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move1.value, 10)
self.assertEqual(move1.remaining_value, 10)
self.assertEqual(move1.remaining_qty, 1)
self.assertAlmostEqual(self.product1.qty_available, 0.0)
self.assertAlmostEqual(self.product1.qty_at_date, 2.0)
self.assertEqual(self.product1.stock_value, 10)
self.assertTrue(len(move1.account_move_ids), 1)
@@ -3152,8 +3261,10 @@ class TestStockValuation(TransactionCase):
move1.move_line_ids.qty_done = 10
move1._action_done()
move1.date = date2
move1.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 10)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 20
@@ -3170,8 +3281,10 @@ class TestStockValuation(TransactionCase):
move2.move_line_ids.qty_done = 20
move2._action_done()
move2.date = date3
move2.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, 30)
self.assertAlmostEqual(self.product1.qty_at_date, 30.0)
self.assertEqual(self.product1.stock_value, 300)
# send 15
@@ -3188,8 +3301,10 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 15
move3._action_done()
move3.date = date4
move3.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, 15)
self.assertAlmostEqual(self.product1.qty_at_date, 15.0)
self.assertEqual(self.product1.stock_value, 150)
# set the standard price to 5
@@ -3197,6 +3312,7 @@ class TestStockValuation(TransactionCase):
self.env['product.price.history'].search([('product_id', '=', self.product1.id)], order='datetime desc, id DESC', limit=1).datetime = date5
self.assertEqual(self.product1.qty_available, 15)
self.assertAlmostEqual(self.product1.qty_at_date, 15.0)
self.assertEqual(self.product1.stock_value, 75)
# send 20
@@ -3213,8 +3329,10 @@ class TestStockValuation(TransactionCase):
move4.move_line_ids.qty_done = 20
move4._action_done()
move4.date = date6
move4.account_move_ids.write({'date': date6})
self.assertEqual(self.product1.qty_available, -5)
self.assertAlmostEqual(self.product1.qty_at_date, -5.0)
self.assertEqual(self.product1.stock_value, -25)
# set the standard price to 7.5
@@ -3235,11 +3353,13 @@ class TestStockValuation(TransactionCase):
move5.move_line_ids.qty_done = 100
move5._action_done()
move5.date = date8
move5.account_move_ids.write({'date': date8})
self.assertEqual(self.product1.qty_available, 95)
self.assertAlmostEqual(self.product1.qty_at_date, 95.0)
self.assertEqual(self.product1.stock_value, 712.5)
# Quantity at date
# Quantity available at date
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_available, 0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_available, 30)
@@ -3257,12 +3377,25 @@ class TestStockValuation(TransactionCase):
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).stock_value, 712.5)
# Quantity at date
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 0.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 30.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 15.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 15.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_at_date, -5.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_at_date, 95.0)
# edit the done quantity of move1, decrease it
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0)
move1.quantity_done = 5
move1.account_move_ids.write({'date': date2})
# the quantity at date will reflect the change directly
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 5)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 5.0)
# as when we decrease a quantity on a recreipt, we consider it as a out move with the price
# of today, the value will be decrease of 100 - (5*7.5)
@@ -3274,12 +3407,17 @@ class TestStockValuation(TransactionCase):
# edit move 4, send 15 instead of 20
# we now have +5 + 20 - 15 -20 = -10 * a standard price of 5
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -10.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -10.0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -50)
move4.quantity_done = 15
move4.account_move_ids.write({'date': date6})
# -(20*5) + (5*7.5)
self.assertEqual(move4.value, -62.5)
# we now have +5 + 20 - 15 -15 = -5 * a standard price of 5
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5.0)
self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0)
self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25)
def test_at_date_fifo_1(self):
@@ -3315,6 +3453,7 @@ class TestStockValuation(TransactionCase):
move1.account_move_ids.write({'date': date1})
self.assertEqual(self.product1.qty_available, 10)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 10@12
@@ -3335,6 +3474,7 @@ class TestStockValuation(TransactionCase):
move2.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 20)
self.assertAlmostEqual(self.product1.qty_at_date, 20)
self.assertEqual(self.product1.stock_value, 220)
# send 15
@@ -3354,6 +3494,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, 5)
self.assertAlmostEqual(self.product1.qty_at_date, 5.0)
self.assertEqual(self.product1.stock_value, 60)
# send 20
@@ -3373,6 +3514,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, -15)
self.assertAlmostEqual(self.product1.qty_at_date, -15.0)
self.assertEqual(self.product1.stock_value, -180)
# receive 100@15
@@ -3393,6 +3535,7 @@ class TestStockValuation(TransactionCase):
move5.account_move_ids.write({'date': date5})
self.assertEqual(self.product1.qty_available, 85)
self.assertAlmostEqual(self.product1.qty_at_date, 85.0)
self.assertEqual(self.product1.stock_value, 1320)
# run the vacuum to compensate the negative stock move
@@ -3400,6 +3543,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids[0].write({'date': date6})
self.assertEqual(self.product1.qty_available, 85)
self.assertAlmostEqual(self.product1.qty_at_date, 85.0)
self.assertEqual(self.product1.stock_value, 1275)
# Edit the quantity done of move1, increase it.
@@ -3472,6 +3616,7 @@ class TestStockValuation(TransactionCase):
move1.account_move_ids.write({'date': date1})
self.assertEqual(self.product1.qty_available, 10)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# receive 10@15
@@ -3492,6 +3637,7 @@ class TestStockValuation(TransactionCase):
move2.account_move_ids.write({'date': date2})
self.assertEqual(self.product1.qty_available, 20)
self.assertAlmostEqual(self.product1.qty_at_date, 20.0)
self.assertEqual(self.product1.stock_value, 250)
# send 30
@@ -3511,6 +3657,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids.write({'date': date3})
self.assertEqual(self.product1.qty_available, -10)
self.assertAlmostEqual(self.product1.qty_at_date, -10.0)
self.assertEqual(self.product1.stock_value, -150)
# receive 10@20
@@ -3531,6 +3678,7 @@ class TestStockValuation(TransactionCase):
move4.account_move_ids.write({'date': date4})
self.assertEqual(self.product1.qty_available, 0)
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertEqual(self.product1.stock_value, 50)
# receive 10@10
@@ -3551,6 +3699,7 @@ class TestStockValuation(TransactionCase):
move5.account_move_ids.write({'date': date5})
self.assertEqual(self.product1.qty_available, 10)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 150)
# run the vacuum to compensate the negative stock move
@@ -3558,6 +3707,7 @@ class TestStockValuation(TransactionCase):
move3.account_move_ids[0].write({'date': date6})
self.assertEqual(self.product1.qty_available, 10)
self.assertAlmostEqual(self.product1.qty_at_date, 10.0)
self.assertEqual(self.product1.stock_value, 100)
# ---------------------------------------------------------------------