Add flat children option on location
issue6563 review36391002
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
------------------
|
||||
|
||||
+19
-3
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -6,13 +6,14 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name"/>
|
||||
<label name="code"/>
|
||||
<field name="code"/>
|
||||
<label name="parent"/>
|
||||
<field name="parent"/>
|
||||
<newline/>
|
||||
<label name="type"/>
|
||||
<field name="type"/>
|
||||
<label name="active"/>
|
||||
<field name="active"/>
|
||||
<label name="parent"/>
|
||||
<field name="parent"/>
|
||||
<label name="flat_childs"/>
|
||||
<field name="flat_childs"/>
|
||||
<newline/>
|
||||
<label name="address"/>
|
||||
<field name="address"/>
|
||||
|
||||
Reference in New Issue
Block a user