diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 36df77a8048936ed6c84596ff7119242e48122cc..5d5631c40694b5a12dd4ff744dd5c591b58c9c48 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -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 diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 42920acb3eb869cdd2ca23a723fd1893becf2202..c121a479a14089e97e367e340a9230512e974e55 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -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'