[FIX] stock_account: in or out move consideration

Only looking on the stock move to determine if a move should create
accounting entries is not enough: it is possible that sublocations
implement some strategies that impact the valuation (example: a move
between stock and stock where the move lines move things to a scrapped
location).

We now look on the move line and if any of the move lines is IN, the move
is IN, if any of the move line is OUT, the move is OUT, if there's a
mix, the move is invalid.
This commit is contained in:
Simon Lejeune
2017-11-14 17:39:08 +01:00
parent 48f4ce4afa
commit 7a4ab354be
2 changed files with 113 additions and 6 deletions
+27 -6
View File
@@ -184,7 +184,10 @@ class StockMove(models.Model):
:return: True if the move is entering the company else False
"""
return not self.location_id._should_be_valued() and self.location_dest_id._should_be_valued()
for move_line in self.move_line_ids:
if not move_line.location_id._should_be_valued() and move_line.location_dest_id._should_be_valued():
return True
return False
def _is_out(self):
""" Check if the move should be considered as leaving the company so that the cost method
@@ -192,7 +195,10 @@ class StockMove(models.Model):
:return: True if the move is leaving the company else False
"""
return self.location_id._should_be_valued() and not self.location_dest_id._should_be_valued()
for move_line in self.move_line_ids:
if move_line.location_id._should_be_valued() and not move_line.location_dest_id._should_be_valued():
return True
return False
@api.model
def _run_fifo(self, move, quantity=None):
@@ -292,6 +298,21 @@ class StockMove(models.Model):
self.product_price_update_before_done()
res = super(StockMove, self)._action_done()
for move in res:
# Apply restrictions on the stock move to be able to make
# consistent accounting entries.
if move._is_in() and move._is_out():
raise UserError(_("The move lines are not in a consistent state: some are entering and other are leaving the company. "))
company_src = move.mapped('move_line_ids.location_id.company_id')
company_dst = move.mapped('move_line_ids.location_dest_id.company_id')
try:
if company_src:
company_src.ensure_one()
if company_dst:
company_dst.ensure_one()
except ValueError:
raise UserError(_("The move lines are not in a consistent states: they do not share the same origin or destination company."))
if company_src and company_dst and company_src.id != company_dst.id:
raise UserError(_("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location."))
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()
@@ -536,12 +557,12 @@ class StockMove(models.Model):
location_from = self.location_id
location_to = self.location_dest_id
company_from = location_from._should_be_valued() and location_from.company_id or False
company_to = location_to._should_be_valued() and location_to.company_id or False
company_from = self._is_out() and self.mapped('move_line_ids.location_id.company_id') or False
company_to = self._is_in() and self.mapped('move_line_ids.location_dest_id.company_id') or False
# Create Journal Entry for products arriving in the company; in case of routes making the link between several
# warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
if company_to and (self._is_in() or company_from != company_to):
if self._is_in():
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
if location_from and location_from.usage == 'customer': # goods returned from customer
self.with_context(force_company=company_to.id)._create_account_move_line(acc_dest, acc_valuation, journal_id)
@@ -549,7 +570,7 @@ class StockMove(models.Model):
self.with_context(force_company=company_to.id)._create_account_move_line(acc_src, acc_valuation, journal_id)
# Create Journal Entry for products leaving the company
if company_from and (self._is_out() or company_from != company_to):
if self._is_out():
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
if location_to and location_to.usage == 'supplier': # goods returned to supplier
self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_src, journal_id)
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
@@ -2734,3 +2735,88 @@ class TestStockValuation(TransactionCase):
move2._action_done()
self.assertEqual(move2.value, -10)
def test_move_in_or_out(self):
""" Test a few combination of move and their move lines and
check their valuation. A valued move should be IN or OUT.
Creating a move that is IN and OUT should be forbidden.
"""
# an internal move should be considered as OUT if any of its move line
# is moved in a scrap location
scrap = self.env['stock.location'].create({
'name': 'scrap',
'usage': 'inventory',
'location_id': self.stock_location.id,
'scrap_location': True,
})
move1 = self.env['stock.move'].create({
'name': 'internal but out move',
'location_id': self.stock_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 2.0,
})
move1._action_confirm()
move1._action_assign()
move1.write({'move_line_ids': [
(0, None, {
'product_id': self.product1.id,
'qty_done': 1,
'location_id': self.stock_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id
}),
(0, None, {
'product_id': self.product1.id,
'qty_done': 1,
'location_id': self.stock_location.id,
'location_dest_id': scrap.id,
'product_uom_id': self.uom_unit.id
}),
]})
self.assertEqual(move1._is_out(), True)
# a move should be considered as invalid if some of its move lines are
# entering the company and some are leaving
customer1 = self.env['stock.location'].create({
'name': 'customer',
'usage': 'customer',
'location_id': self.stock_location.id,
})
supplier1 = self.env['stock.location'].create({
'name': 'supplier',
'usage': 'supplier',
'location_id': self.stock_location.id,
})
move2 = self.env['stock.move'].create({
'name': 'internal but in and out move',
'location_id': self.stock_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 2.0,
})
move2._action_confirm()
move2._action_assign()
move2.write({'move_line_ids': [
(0, None, {
'product_id': self.product1.id,
'qty_done': 1,
'location_id': customer1.id,
'location_dest_id': self.stock_location.id,
'product_uom_id': self.uom_unit.id
}),
(0, None, {
'product_id': self.product1.id,
'qty_done': 1,
'location_id': self.stock_location.id,
'location_dest_id': customer1.id,
'product_uom_id': self.uom_unit.id
}),
]})
self.assertEqual(move2._is_in(), True)
self.assertEqual(move2._is_out(), True)
with self.assertRaises(UserError):
move2._action_done()