diff --git a/CHANGELOG b/CHANGELOG
index cf39ad236a..dfded08563 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add flat children option on location
* Raise single warning for move without origin
* Allow to set manual effective date on move
* Improve computation of cost price
diff --git a/doc/index.rst b/doc/index.rst
index 1e0c637e8f..1c95705a29 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -57,6 +57,8 @@ virtually stored. The following location types are defined:
Locations are organised in tree structures, allowing to define
fine grained structures.
+It is possible to restrict a location to have only one level of children, this
+allows to improve the performance of the stock quantity computation.
Location Lead Time
------------------
diff --git a/location.py b/location.py
index 77884ba28d..6556b84343 100644
--- a/location.py
+++ b/location.py
@@ -54,6 +54,9 @@ class Location(ModelSQL, ModelView):
left = fields.Integer('Left', required=True, select=True)
right = fields.Integer('Right', required=True, select=True)
childs = fields.One2Many("stock.location", "parent", "Children")
+ flat_childs = fields.Boolean(
+ "Flat Children",
+ help="Check to restrict to one level of children.")
warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'),
'get_warehouse')
input_location = fields.Many2One(
@@ -126,8 +129,17 @@ class Location(ModelSQL, ModelView):
"to be deactivated."),
})
- parent_domain = []
- childs_domain = []
+ parent_domain = [
+ ['OR',
+ ('parent.flat_childs', '=', False),
+ ('parent', '=', None),
+ ]
+ ]
+ childs_domain = [
+ If(Eval('flat_childs', False),
+ ('childs', '=', None),
+ ()),
+ ]
childs_mapping = cls._childs_domain()
for type_, allowed_parents in cls._parent_domain().iteritems():
parent_domain.append(If(Eval('type') == type_,
@@ -136,7 +148,7 @@ class Location(ModelSQL, ModelView):
('type', 'in', childs_mapping[type_]), ()))
cls.parent.domain = parent_domain
cls.childs.domain = childs_domain
- cls.childs.depends.append('type')
+ cls.childs.depends.extend(['flat_childs', 'type'])
@classmethod
def _parent_domain(cls):
@@ -256,6 +268,10 @@ class Location(ModelSQL, ModelView):
def default_right():
return 0
+ @classmethod
+ def default_flat_childs(cls):
+ return False
+
@staticmethod
def default_type():
return 'storage'
diff --git a/move.py b/move.py
index 745ef35812..86713a2a20 100644
--- a/move.py
+++ b/move.py
@@ -9,7 +9,7 @@ from itertools import groupby
from sql import Literal, Union, Column, Null, For
from sql.aggregate import Sum
-from sql.conditionals import Coalesce
+from sql.conditionals import Coalesce, Case
from sql.operators import Concat
from trytond.model import Workflow, Model, ModelView, ModelSQL, fields, Check
@@ -1012,6 +1012,84 @@ class Move(Workflow, ModelSQL, ModelView):
if PeriodCache:
period_cache = PeriodCache.__table__()
+ if with_childs:
+ # Replace tables with union which replaces flat children locations
+ # by their parent location.
+ from_location = Location.__table__()
+ from_parent_location = Location.__table__()
+ to_location = Location.__table__()
+ to_parent_location = Location.__table__()
+ columns = ['id', 'state', 'effective_date', 'planned_date',
+ 'internal_quantity'] + list(grouping)
+ columns = [Column(move, c).as_(c) for c in columns]
+
+ move_with_parent = (move
+ .join(from_location,
+ condition=move.from_location == from_location.id)
+ .join(from_parent_location, type_='LEFT',
+ condition=from_location.parent == from_parent_location.id)
+ .join(to_location,
+ condition=move.to_location == to_location.id)
+ .join(to_parent_location, type_='LEFT',
+ condition=to_location.parent == to_parent_location.id))
+
+ move = Union(
+ # Moves not linked to any flat location
+ move_with_parent.select(
+ move.from_location.as_('from_location'),
+ move.to_location.as_('to_location'),
+ *columns,
+ where=(Coalesce(from_parent_location.flat_childs, False)
+ != True)
+ & (Coalesce(to_parent_location.flat_childs, False)
+ != True)),
+ # Append moves linked from/to flat locations to their parents
+ move_with_parent.select(
+ Case(
+ (from_parent_location.flat_childs,
+ from_parent_location.id),
+ else_=move.from_location).as_('from_location'),
+ Case(
+ (to_parent_location.flat_childs,
+ to_parent_location.id),
+ else_=move.to_location).as_('to_location'),
+ *columns,
+ where=(from_parent_location.flat_childs == True)
+ | (to_parent_location.flat_childs == True)),
+ # Append moves linked to from/to flat locations only
+ move_with_parent.select(
+ Case(
+ (from_parent_location.flat_childs,
+ from_location.id),
+ else_=Null).as_('from_location'),
+ Case(
+ (to_parent_location.flat_childs,
+ to_location.id),
+ else_=Null).as_('to_location'),
+ *columns,
+ where=(from_parent_location.flat_childs == True)
+ | (to_parent_location.flat_childs == True)),
+ all_=True)
+
+ if PeriodCache:
+ location = Location.__table__()
+ parent_location = Location.__table__()
+ columns = ['internal_quantity', 'period'] + list(grouping)
+ columns = [Column(period_cache, c).as_(c) for c in columns]
+ period_cache = Union(
+ period_cache.select(
+ period_cache.location.as_('location'),
+ *columns),
+ period_cache.join(location,
+ condition=period_cache.location == location.id
+ ).join(parent_location, type_='LEFT',
+ condition=location.parent == parent_location.id
+ ).select(
+ parent_location.id.as_('location'),
+ *columns,
+ where=parent_location.flat_childs == True),
+ all_=True)
+
if not context.get('stock_date_end'):
context['stock_date_end'] = datetime.date.max
@@ -1152,9 +1230,7 @@ class Move(Workflow, ModelSQL, ModelView):
state_date_clause_out &= state_date_clause()
if with_childs:
- location_query = Location.search([
- ('parent', 'child_of', location_ids),
- ], query=True, order=[])
+ location_query = _location_children(location_ids, query=True)
else:
location_query = location_ids[:]
@@ -1251,7 +1327,6 @@ class Move(Workflow, ModelSQL, ModelView):
and quantity as value.
"""
pool = Pool()
- Location = pool.get('stock.location')
Product = pool.get('product.product')
assert query is not None, (
@@ -1283,15 +1358,13 @@ class Move(Workflow, ModelSQL, ModelView):
# Propagate quantities on from child locations to their parents
if with_childs and len(location_ids) > 1:
# Fetch all child locations
- locations = Location.search([
- ('parent', 'child_of', location_ids),
- ])
+ locations = _location_children(location_ids)
# Generate a set of locations without childs and a dict
# giving the parent of each location.
leafs = set([l.id for l in locations])
parent = {}
for location in locations:
- if not location.parent:
+ if not location.parent or location.parent.flat_childs:
continue
if location.parent.id in leafs:
leafs.remove(location.parent.id)
@@ -1330,3 +1403,26 @@ class Move(Workflow, ModelSQL, ModelView):
quantities[key] = uom.round(quantity)
return quantities
+
+
+def _location_children(location_ids, query=False):
+ "Return children location without including flat children"
+ pool = Pool()
+ Location = pool.get('stock.location')
+ nested_location_ids = []
+ flat_location_ids = []
+ for location in Location.browse(location_ids):
+ if location.flat_childs:
+ flat_location_ids.append(location.id)
+ else:
+ nested_location_ids.append(location.id)
+ if nested_location_ids:
+ return Location.search(['OR',
+ ('parent', 'child_of', nested_location_ids),
+ ('id', 'in', flat_location_ids),
+ ], query=query, order=[])
+ else:
+ if query:
+ return flat_location_ids
+ else:
+ return Location.browse(flat_location_ids)
diff --git a/tests/test_stock.py b/tests/test_stock.py
index bc336e50bd..158a2b0375 100644
--- a/tests/test_stock.py
+++ b/tests/test_stock.py
@@ -442,6 +442,115 @@ class StockTestCase(ModuleTestCase):
])
self.assertListEqual([product], found_products)
+ @with_transaction()
+ def test_products_by_location_flat_childs(self, period_closed=False):
+ "Test products_by_location on flat_childs"
+ pool = Pool()
+ Uom = pool.get('product.uom')
+ Template = pool.get('product.template')
+ Product = pool.get('product.product')
+ Location = pool.get('stock.location')
+ Move = pool.get('stock.move')
+ Date = pool.get('ir.date')
+ Period = pool.get('stock.period')
+
+ unit, = Uom.search([('name', '=', 'Unit')])
+ template, = Template.create([{
+ 'name': "Product",
+ 'type': 'goods',
+ 'list_price': Decimal(0),
+ 'default_uom': unit.id,
+ }])
+ product, = Product.create([{
+ 'template': template.id,
+ }])
+
+ lost_found, = Location.search([('type', '=', 'lost_found')])
+ storage, = Location.search([('code', '=', 'STO')])
+ storage.flat_childs = True
+ storage.save()
+ storage1, = Location.create([{
+ 'name': 'Storage 1',
+ 'type': 'storage',
+ 'parent': storage.id,
+ }])
+ storage2, = Location.create([{
+ 'name': 'Storage 2',
+ 'type': 'storage',
+ 'parent': storage.id,
+ }])
+
+ company = create_company()
+ with set_company(company):
+ date = Date.today() - relativedelta(days=1)
+
+ moves = Move.create([{
+ 'product': product.id,
+ 'uom': unit.id,
+ 'quantity': 1,
+ 'from_location': lost_found.id,
+ 'to_location': storage.id,
+ 'planned_date': date,
+ 'effective_date': date,
+ 'company': company.id,
+ }, {
+ 'product': product.id,
+ 'uom': unit.id,
+ 'quantity': 1,
+ 'from_location': lost_found.id,
+ 'to_location': storage1.id,
+ 'planned_date': date,
+ 'effective_date': date,
+ 'company': company.id,
+ }, {
+ 'product': product.id,
+ 'uom': unit.id,
+ 'quantity': 1,
+ 'from_location': storage1.id,
+ 'to_location': storage2.id,
+ 'planned_date': date,
+ 'effective_date': date,
+ 'company': company.id,
+ }])
+ Move.do(moves)
+
+ if period_closed:
+ period, = Period.create([{
+ 'date': date,
+ 'company': company.id,
+ }])
+ Period.close([period])
+
+ # Test flat location
+ products_by_location = Product.products_by_location(
+ [storage.id], [product.id], with_childs=True)
+ self.assertEqual(
+ products_by_location[(storage.id, product.id)], 2)
+
+ # Test mixed flat and nested location
+ products_by_location = Product.products_by_location(
+ [storage.parent.id, storage.id, storage1.id, storage2.id],
+ [product.id],
+ with_childs=True)
+ self.assertEqual(
+ products_by_location[(storage.parent.id, product.id)], 2)
+ self.assertEqual(
+ products_by_location[(storage.id, product.id)], 2)
+ self.assertEqual(
+ products_by_location[(storage1.id, product.id)], 0)
+ self.assertEqual(
+ products_by_location[(storage2.id, product.id)], 1)
+
+ # Test non flat
+ products_by_location = Product.products_by_location(
+ [lost_found.id], [product.id], with_childs=True)
+ self.assertEqual(
+ products_by_location[(lost_found.id, product.id)], -2)
+
+ def test_products_by_location_flat_childs_period_closed(self):
+ "Test products_by_location on flat_childs with period closed"
+ self.test_products_by_location_flat_childs(period_closed=True)
+
@with_transaction()
def test_period(self):
'Test period'
diff --git a/view/location_form.xml b/view/location_form.xml
index 7e0e3f247f..5134360cfb 100644
--- a/view/location_form.xml
+++ b/view/location_form.xml
@@ -6,13 +6,14 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
+
+
+
+