[REF] stock_account

- value move lines created when the move is done
- when reducing done out move, increase latest in move in order to not
  lose the returned quantity in the fifo chain
This commit is contained in:
Simon Lejeune
2017-10-03 17:15:54 +02:00
parent 29c326a8aa
commit 9eac6c6377
2 changed files with 426 additions and 39 deletions
+65 -36
View File
@@ -52,6 +52,16 @@ class StockLocation(models.Model):
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
@api.model
def create(self, vals):
res = super(StockMoveLine, self).create(vals)
move = res.move_id
if move.state == 'done':
correction_value = move._run_valuation(res.qty_done)
if move.product_id.valuation == 'real_time' and (move._is_in() or move._is_out()):
move.with_context(force_valuation_amount=correction_value)._account_entry_move()
return res
@api.multi
def write(self, vals):
@@ -66,23 +76,37 @@ class StockMoveLine(models.Model):
# more/less units are available, update `remaining_value` and
# `remaining_qty` on the linked stock move.
move_vals = {'remaining_qty': move_id.remaining_qty + qty_difference}
new_remaining_value = 0
if move_id.product_id.cost_method in ['standard', 'average']:
correction_value = qty_difference * move_id.product_id.standard_price
move_vals['value'] = move_id.value - correction_value
move_vals.pop('remaining_qty')
else:
# FIFO handling
if move_id._is_in():
correction_value = qty_difference * move_id.price_unit
new_remaining_value = move_id.remaining_value + correction_value
elif move_id._is_out() and qty_difference > 0:
# send more, run fifo again
correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference)
new_remaining_value = move_id.remaining_value + correction_value
move_vals.pop('remaining_qty')
elif move_id._is_out() and qty_difference < 0:
# return, value at last fifo price
correction_value = qty_difference * move_id.product_id.standard_price
remaining_value = move_id.remaining_value + correction_value
# fake return, find the last receipt and augment its qties
candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1)
if candidates_receipt:
candidates_receipt.write({
'remaining_qty': candidates_receipt.remaining_qty + -qty_difference,
'remaining_value': candidates_receipt.remaining_value + (-qty_difference * candidates_receipt.price_unit),
})
correction_value = qty_difference * candidates_receipt.price_unit
else:
correction_value = qty_difference * move_id.product_id.standard_price
move_vals.pop('remaining_qty')
if move_id._is_out():
move_vals['remaining_value'] = remaining_value if remaining_value < 0 else 0
move_vals['remaining_value'] = new_remaining_value if new_remaining_value < 0 else 0
else:
move_vals['remaining_value'] = remaining_value
move_vals['remaining_value'] = new_remaining_value
move_id.write(move_vals)
if move_id.product_id.valuation == 'real_time':
@@ -201,52 +225,57 @@ class StockMove(models.Model):
# last out and a correction entry will be made once `_fifo_vacuum` is called.
if qty_to_take_on_candidates == 0:
move.write({
'value': -tmp_value, # outgoing move are valued negatively
'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively
'price_unit': -tmp_value / move.product_qty,
})
elif qty_to_take_on_candidates > 0:
last_fifo_price = new_standard_price or move.product_id.standard_price
negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
vals = {
'remaining_qty': -qty_to_take_on_candidates,
'remaining_value': negative_stock_value,
'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates,
'remaining_value': move.remaining_value + negative_stock_value,
'value': -tmp_value + negative_stock_value,
'price_unit': (-tmp_value + negative_stock_value) / move.product_qty,
'price_unit': (-tmp_value + negative_stock_value) / (move.product_qty or quantity),
}
move.write(vals)
return tmp_value
def _run_valuation(self, quantity=None):
self.ensure_one()
if self._is_in():
if self.product_id.cost_method in ['fifo', 'average']:
price_unit = self.price_unit or self._get_price_unit()
value = price_unit * (quantity or self.product_qty)
vals = {
'price_unit': price_unit,
'value': value if quantity is None or not self.value else self.value,
'remaining_value': value if quantity is None else self.remaining_value + value,
}
if self.product_id.cost_method == 'fifo':
vals['remaining_qty'] = self.product_qty if quantity is None else self.remaining_qty + quantity
self.write(vals)
else: # standard
value = self.product_id.standard_price * (quantity or self.product_qty)
self.write({
'price_unit': self.product_id.standard_price,
'value': value if quantity is None or not self.value else self.value,
})
elif self._is_out():
if self.product_id.cost_method == 'fifo':
self.env['stock.move']._run_fifo(self, quantity=quantity)
elif self.product_id.cost_method in ['standard', 'average']:
curr_rounding = self.company_id.currency_id.rounding
value = -float_round(self.product_id.standard_price * (self.product_qty if quantity is None else quantity), precision_rounding=curr_rounding)
self.write({
'value': value if quantity is None else self.value + value,
'price_unit': value / self.product_qty,
})
def _action_done(self):
self.product_price_update_before_done()
res = super(StockMove, self)._action_done()
for move in res:
if move._is_in():
if move.product_id.cost_method in ['fifo', 'average']:
price_unit = move.price_unit or move._get_price_unit()
value = price_unit * move.product_qty
vals = {
'price_unit': price_unit,
'value': value,
'remaining_value': value,
}
if move.product_id.cost_method == 'fifo':
vals['remaining_qty'] = move.product_qty
move.write(vals)
else: # standard
move.write({
'price_unit': move.product_id.standard_price,
'value': move.product_id.standard_price * move.product_qty,
})
elif move._is_out():
if move.product_id.cost_method == 'fifo':
self.env['stock.move']._run_fifo(move)
elif move.product_id.cost_method in ['standard', 'average']:
curr_rounding = move.company_id.currency_id.rounding
value = -float_round(move.product_id.standard_price * move.product_qty, precision_rounding=curr_rounding)
move.write({
'value': value,
'price_unit': value / move.product_qty,
})
move._run_valuation()
for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out())):
move._account_entry_move()
return res
@@ -10,14 +10,21 @@ class TestStockValuation(TransactionCase):
self.stock_location = self.env.ref('stock.stock_location_stock')
self.customer_location = self.env.ref('stock.stock_location_customers')
self.supplier_location = self.env.ref('stock.stock_location_suppliers')
self.partner = self.env['res.partner'].create({'name': 'xxx'})
self.uom_unit = self.env.ref('product.product_uom_unit')
self.product1 = self.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.product2 = self.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.product1.product_tmpl_id.valuation = 'real_time'
self.product2.product_tmpl_id.valuation = 'real_time'
Account = self.env['account.account']
self.stock_input_account = Account.create({
'name': 'Stock Input',
@@ -54,19 +61,16 @@ class TestStockValuation(TransactionCase):
def _get_stock_input_move_lines(self):
return self.env['account.move.line'].search([
('product_id', '=', self.product1.id),
('account_id', '=', self.stock_input_account.id),
], order='date, id')
def _get_stock_output_move_lines(self):
return self.env['account.move.line'].search([
('product_id', '=', self.product1.id),
('account_id', '=', self.stock_output_account.id),
], order='date, id')
def _get_stock_valuation_move_lines(self):
return self.env['account.move.line'].search([
('product_id', '=', self.product1.id),
('account_id', '=', self.stock_valuation_account.id),
], order='date, id')
@@ -1480,6 +1484,272 @@ class TestStockValuation(TransactionCase):
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 310)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
def test_fifo_add_move_in_done_picking_1(self):
self.product1.product_tmpl_id.cost_method = 'fifo'
# ---------------------------------------------------------------------
# Receive 10@10
# ---------------------------------------------------------------------
receipt = self.env['stock.picking'].create({
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'partner_id': self.partner.id,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
})
move1 = self.env['stock.move'].create({
'picking_id': receipt.id,
'name': '10 in',
'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': 10.0,
'price_unit': 10,
'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 10.0,
})]
})
move1._action_confirm()
move1._action_done()
# stock values for move1
self.assertEqual(move1.value, 100.0)
self.assertEqual(move1.remaining_qty, 10.0)
self.assertEqual(move1.price_unit, 10.0)
self.assertEqual(move1.remaining_value, 100.0)
# ---------------------------------------------------------------------
# Add a stock move, receive 10@20 of another product
# ---------------------------------------------------------------------
self.product2.product_tmpl_id.cost_method = 'fifo'
self.product2.standard_price = 20
move2 = self.env['stock.move'].create({
'picking_id': receipt.id,
'name': '10 in',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'state': 'done', # simulate default_get override
'move_line_ids': [(0, 0, {
'product_id': self.product2.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 10.0,
})]
})
self.assertEqual(move2.value, 200.0)
self.assertEqual(move2.remaining_qty, 10.0)
self.assertEqual(move2.price_unit, 20.0)
self.assertEqual(move2.remaining_value, 200.0)
self.assertEqual(self.product1.qty_available, 10)
self.assertEqual(self.product1.stock_value, 100)
self.assertEqual(self.product2.qty_available, 10)
self.assertEqual(self.product2.stock_value, 200)
# ---------------------------------------------------------------------
# Edit the previous stock move, receive 11
# ---------------------------------------------------------------------
move2.quantity_done = 11
self.assertEqual(move2.value, 200.0)
self.assertEqual(move2.remaining_qty, 11.0)
self.assertEqual(move2.price_unit, 20.0)
self.assertEqual(move2.remaining_value, 220.0)
# ---------------------------------------------------------------------
# Send 11 product 2
# ---------------------------------------------------------------------
delivery = self.env['stock.picking'].create({
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'partner_id': self.partner.id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
})
move3 = self.env['stock.move'].create({
'picking_id': delivery.id,
'name': '11 out',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 11.0,
'move_line_ids': [(0, 0, {
'product_id': self.product2.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 11.0,
})]
})
move3._action_confirm()
move3._action_done()
self.assertEqual(move3.value, -220.0)
self.assertEqual(move3.remaining_qty, 0.0)
self.assertEqual(move3.price_unit, -20.0)
self.assertEqual(move3.remaining_value, 0.0)
# ---------------------------------------------------------------------
# Add one move of product 2
# ---------------------------------------------------------------------
move4 = self.env['stock.move'].create({
'picking_id': delivery.id,
'name': '1 out',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'state': 'done', # simulate default_get override
'move_line_ids': [(0, 0, {
'product_id': self.product2.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 1.0,
})]
})
self.assertEqual(move4.value, -20.0)
self.assertEqual(move4.remaining_qty, -1.0)
self.assertEqual(move4.price_unit, -20.0)
self.assertEqual(move4.remaining_value, -20.0)
# ---------------------------------------------------------------------
# edit the created move, add 1
# ---------------------------------------------------------------------
move4.quantity_done = 2
self.assertEqual(move4.value, -20.0)
self.assertEqual(move4.remaining_qty, -2.0)
self.assertEqual(move4.price_unit, -20.0)
self.assertEqual(move4.remaining_value, -40.0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 320) # 10*10 + 11*20
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 260)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
self.env['stock.move']._run_fifo_vacuum()
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 320) # 10*10 + 11*20
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 260)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
# ---------------------------------------------------------------------
# receive 2 products 2 @ 30
# ---------------------------------------------------------------------
move1 = self.env['stock.move'].create({
'picking_id': receipt.id,
'name': '10 in',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 2.0,
'price_unit': 30,
'move_line_ids': [(0, 0, {
'product_id': self.product2.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 2.0,
})]
})
move1._action_confirm()
move1._action_done()
# ---------------------------------------------------------------------
# run vacuum
# ---------------------------------------------------------------------
self.env['stock.move']._run_fifo_vacuum()
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 380) # 10*10 + 11*20
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 280) # 260/
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 280)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
self.assertEqual(self.product2.qty_available, 0)
self.assertEqual(self.product2.stock_value, 0)
self.assertEqual(move4.remaining_value, 0)
def test_fifo_add_moveline_in_done_move_1(self):
self.product1.product_tmpl_id.cost_method = 'fifo'
# ---------------------------------------------------------------------
# Receive 10@10
# ---------------------------------------------------------------------
move1 = self.env['stock.move'].create({
'name': '10 in',
'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': 10.0,
'price_unit': 10,
'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 10.0,
})]
})
move1._action_confirm()
move1._action_done()
# stock values for move1
self.assertEqual(move1.value, 100.0)
self.assertEqual(move1.remaining_qty, 10.0)
self.assertEqual(move1.price_unit, 10.0)
self.assertEqual(move1.remaining_value, 100.0)
self.assertEqual(len(move1.account_move_ids), 1)
# ---------------------------------------------------------------------
# Add a new move line to receive 10 more
# ---------------------------------------------------------------------
self.assertEqual(len(move1.move_line_ids), 1)
self.env['stock.move.line'].with_context(debug=True).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(move1.value, 100.0)
self.assertEqual(move1.remaining_qty, 20.0)
self.assertEqual(move1.price_unit, 10.0)
self.assertEqual(move1.remaining_value, 200.0)
self.assertEqual(len(move1.account_move_ids), 2)
self.assertEqual(self.product1.qty_available, 20)
self.assertEqual(self.product1.stock_value, 200)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 200)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 200)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
def test_fifo_edit_done_move1(self):
""" Check that incrementing the done quantity will correctly re-run a fifo lookup.
"""
@@ -1649,6 +1919,94 @@ class TestStockValuation(TransactionCase):
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 148)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
def test_fifo_edit_done_move2(self):
self.product1.product_tmpl_id.cost_method = 'fifo'
# ---------------------------------------------------------------------
# Receive 10@10
# ---------------------------------------------------------------------
move1 = self.env['stock.move'].create({
'name': 'receive 10@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': 10.0,
'price_unit': 10,
'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 10.0,
})]
})
move1._action_confirm()
move1._action_done()
# stock values for move1
self.assertEqual(move1.value, 100.0)
self.assertEqual(move1.remaining_qty, 10.0)
self.assertEqual(move1.price_unit, 10.0)
self.assertEqual(move1.remaining_value, 100.0)
# ---------------------------------------------------------------------
# Send 10
# ---------------------------------------------------------------------
move2 = self.env['stock.move'].create({
'name': '12 out (2 negative)',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'price_unit': 0,
'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 10.0,
})]
})
move2._action_confirm()
move2._action_done()
# stock values for move2
self.assertEqual(move2.value, -100.0)
self.assertEqual(move2.remaining_qty, 0.0)
self.assertEqual(move2.remaining_value, 0.0)
# ---------------------------------------------------------------------
# Actually, send 8 in the last move
# ---------------------------------------------------------------------
move2.quantity_done = 8
self.assertEqual(move2.value, -100.0)
self.assertEqual(move2.remaining_qty, 0.0)
self.assertEqual(move2.remaining_value, 0.0)
self.assertEqual(move1.remaining_qty, 2.0)
self.assertEqual(move1.remaining_value, 20.0)
self.product1.qty_available = 2
self.product1.stock_value = 20
# ---------------------------------------------------------------------
# Actually, send 10 in the last move
# ---------------------------------------------------------------------
move2.with_context(debug=True).quantity_done = 10
self.assertEqual(move2.value, -100.0)
self.assertEqual(move2.remaining_qty, 0.0)
self.assertEqual(move2.remaining_value, 0.0)
self.assertEqual(move1.remaining_qty, 0.0)
self.assertEqual(move1.remaining_value, 0.0)
self.product1.qty_available = 2
self.product1.stock_value = 20
def test_average_perpetual_1(self):
# http://accountingexplained.com/financial/inventories/avco-method
self.product1.product_tmpl_id.cost_method = 'average'