Allow to group per product columns to compute quantity

This is needed to add the quantity relates on the template form.

issue7068
review40031002
This commit is contained in:
Cédric Krier committed 2018-02-11 14:38:33 +01:00
1 parent ae3358481d
commit 55082d76a6
7 files changed
+238 -55

No files matched your search

+3
View File
@@ -1,3 +1,6 @@
* Add quantities relates on Template
* Allow grouping per product columns to compute quantities
Version 4.6.0 - 2017-10-30
* Bug fixes (see mercurial logs for details)
* Add flat children option on location
+50 -28
View File
@@ -304,62 +304,84 @@ class Location(ModelSQL, ModelView):
pool = Pool()
Product = pool.get('product.product')
Date_ = pool.get('ir.date')
trans_context = Transaction().context
if (not Transaction().context.get('product')) \
or not (isinstance(Transaction().context['product'],
(int, long))):
return dict([(l.id, 0) for l in locations])
def valid_context(name):
return (trans_context.get(name) is not None
and isinstance(trans_context[name], (int, long)))
with Transaction().set_context(active_test=False):
if not Product.search([
('id', '=', Transaction().context['product']),
]):
return dict([(l.id, 0) for l in locations])
if not any(map(valid_context, ['product', 'product_template'])):
return {l.id: None for l in locations}
context = {}
if (name == 'quantity'
and Transaction().context.get('stock_date_end') >
Date_.today()):
and trans_context.get('stock_date_end') > Date_.today()):
context['stock_date_end'] = Date_.today()
if name == 'forecast_quantity':
context['forecast'] = True
if not Transaction().context.get('stock_date_end'):
if not trans_context.get('stock_date_end'):
context['stock_date_end'] = datetime.date.max
product_id = Transaction().context['product']
if trans_context.get('product') is not None:
product_ids = [trans_context['product']]
grouping = None
grouping_filter = None
key = trans_context['product']
else:
product_ids = None
grouping = ('product.template',)
grouping_filter = ([trans_context['product_template']],)
key = trans_context['product_template']
pbl = {}
for sub_locations in grouped_slice(locations):
location_ids = [l.id for l in sub_locations]
with Transaction().set_context(context):
pbl.update(Product.products_by_location(
location_ids=location_ids, product_ids=[product_id],
location_ids=location_ids,
product_ids=product_ids,
grouping=grouping,
grouping_filter=grouping_filter,
with_childs=True))
return dict((loc.id, pbl.get((loc.id, product_id), 0))
for loc in locations)
return dict((loc.id, pbl.get((loc.id, key), 0)) for loc in locations)
@classmethod
def get_cost_value(cls, locations, name):
Product = Pool().get('product.product')
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
trans_context = Transaction().context
product_id = trans_context.get('product')
if not product_id:
return dict((l.id, None) for l in locations)
cost_values, context = {}, {}
cost_values = {l.id: None for l in locations}
def valid_context(name):
return (trans_context.get(name) is not None
and isinstance(trans_context[name], (int, long)))
if not any(map(valid_context, ['product', 'product_template'])):
return cost_values
context = {}
if 'stock_date_end' in trans_context:
# Use the last cost_price of the day
context['_datetime'] = datetime.datetime.combine(
trans_context['stock_date_end'], datetime.time.max)
with Transaction().set_context(context):
product = Product(product_id)
if trans_context.get('product') is not None:
product = Product(trans_context['product'])
cost_price = product.cost_price
else:
template = Template(trans_context['product_template'])
cost_price = template.cost_price
# The date could be before the product creation
# or the template may have more than one product
if not isinstance(cost_price, Decimal):
return cost_values
for location in locations:
# The date could be before the product creation
if not isinstance(product.cost_price, Decimal):
cost_values[location.id] = None
else:
cost_values[location.id] = (Decimal(str(location.quantity))
* product.cost_price)
cost_values[location.id] = (
Decimal(str(location.quantity)) * cost_price)
return cost_values
@classmethod
+52 -11
View File
@@ -980,8 +980,8 @@ class Move(Workflow, ModelSQL, ModelView):
quantities of all child locations but quantities of the storage
zone.
If with_childs, it computes also for child locations.
grouping is a tuple of Move field names and defines how stock moves are
grouped.
grouping is a tuple of Move (or Product if prefixed by 'product.')
field names and defines how stock moves are grouped.
grouping_filter is a tuple of values, for the Move's field at the same
position in grouping tuple, used to filter which moves are used to
compute quantities. It must be None or have the same number of
@@ -997,6 +997,7 @@ class Move(Workflow, ModelSQL, ModelView):
Date = pool.get('ir.date')
Period = pool.get('stock.period')
Move = pool.get('stock.move')
Product = pool.get('product.product')
move = Move.__table__()
today = Date.today()
@@ -1005,17 +1006,51 @@ class Move(Workflow, ModelSQL, ModelView):
return None
context = Transaction().context.copy()
use_product = False
for field in grouping:
if field not in Move._fields:
raise ValueError('"%s" has no field "%s"' % (Move, field))
if field.startswith('product.'):
Model = Product
field = field[len('product.'):]
use_product = True
else:
Model = Move
if field not in Model._fields:
raise ValueError('"%s" has no field "%s"' % (Model, field))
assert grouping_filter is None or len(grouping_filter) == len(grouping)
move_rule_query = Rule.query_get('stock.move')
def get_column(name, table, product):
if name.startswith('product.'):
column = Column(product, name[len('product.'):])
else:
column = Column(table, name)
return column.as_(name)
if use_product:
product = Product.__table__()
columns = ['id', 'state', 'effective_date', 'planned_date',
'internal_quantity', 'from_location', 'to_location']
columns += list(grouping)
columns = [get_column(c, move, product) for c in columns]
move = (move
.join(product, condition=move.product == product.id)
.select(*columns))
PeriodCache = Period.get_cache(grouping)
period = None
if PeriodCache:
period_cache = PeriodCache.__table__()
if use_product:
product_cache = Product.__table__()
columns = ['internal_quantity', 'period', 'location']
columns += list(grouping)
columns = [get_column(c, period_cache, product_cache)
for c in columns]
period_cache = (period_cache
.join(product_cache,
condition=period_cache.product == product_cache.id)
.select(*columns))
if with_childs:
# Replace tables with union which replaces flat children locations
@@ -1333,17 +1368,24 @@ class Move(Workflow, ModelSQL, ModelView):
"""
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
assert query is not None, (
"Query in Move.compute_quantities() can't be None")
assert 'product' in grouping
assert 'product' in grouping or 'product.template' in grouping
cursor = Transaction().connection.cursor()
cursor.execute(*query)
raw_lines = cursor.fetchall()
product_getter = operator.itemgetter(grouping.index('product') + 1)
res_product_ids = set()
if 'product' in grouping:
id_name = 'product'
Model = Product
else:
id_name = 'product.template'
Model = Template
id_getter = operator.itemgetter(grouping.index(id_name) + 1)
ids = set()
quantities = defaultdict(lambda: 0)
keys = set()
# We can do a quick loop without propagation if the request is for a
@@ -1357,7 +1399,7 @@ class Move(Workflow, ModelSQL, ModelView):
key = tuple(line[1:-1])
quantity = line[-1]
quantities[(location,) + key] += quantity
res_product_ids.add(product_getter(line))
ids.add(id_getter(line))
keys.add(key)
# Propagate quantities on from child locations to their parents
@@ -1400,11 +1442,10 @@ class Move(Workflow, ModelSQL, ModelView):
# Round quantities
default_uom = dict((p.id, p.default_uom) for p in
Product.browse(list(res_product_ids)))
Model.browse(list(ids)))
for key, quantity in quantities.iteritems():
location = key[0]
product = product_getter(key)
uom = default_uom[product]
uom = default_uom[id_getter(key)]
quantities[key] = uom.round(quantity)
return quantities
+1 -1
View File
@@ -69,7 +69,7 @@ class Period(Workflow, ModelSQL, ModelView):
@staticmethod
def get_cache(grouping):
pool = Pool()
if grouping == ('product',):
if all(g == 'product' or g.startswith('product.') for g in grouping):
return pool.get('stock.period.cache')
def get_rec_name(self, name):
+45 -11
View File
@@ -133,7 +133,7 @@ class Product(StockMixin, object):
@classmethod
def products_by_location(cls, location_ids, product_ids=None,
with_childs=False, grouping=('product',)):
with_childs=False, grouping=None, grouping_filter=None):
"""
Compute for each location and product the stock quantity in the default
uom of the product.
@@ -166,7 +166,12 @@ class Product(StockMixin, object):
wh_to_add[location.id] = location.storage_location.id
location_ids = list(location_ids)
grouping_filter = (product_ids,) + tuple(None for k in grouping[1:])
if grouping is None or grouping == ('product',):
grouping = ('product',)
grouping_filter = (product_ids,)
elif product_ids:
grouping = ('product',) + tuple(grouping)
grouping_filter = (product_ids,) + tuple(grouping_filter)
query = Move.compute_quantities_query(location_ids, with_childs,
grouping=grouping, grouping_filter=grouping_filter)
if query is None:
@@ -303,23 +308,32 @@ class ProductQuantitiesByWarehouse(ModelSQL, ModelView):
pool = Pool()
Move = pool.get('stock.move')
Location = pool.get('stock.location')
move = Move.__table__()
Product = pool.get('product.product')
move = from_ = Move.__table__()
context = Transaction().context
product_id = Transaction().context.get('product')
warehouse_id = Transaction().context.get('warehouse', -1)
if context.get('product_template') is not None:
product = Product.__table__()
from_ = move.join(product, condition=move.product == product.id)
product_clause = (
product.template == context['product_template'])
else:
product_clause = move.product == context.get('product', -1)
warehouse_id = context.get('warehouse', -1)
warehouse_query = Location.search([
('parent', 'child_of', [warehouse_id]),
], query=True, order=[])
date_column = Coalesce(move.effective_date, move.planned_date
).as_('date')
return move.select(
return from_.select(
Max(move.id).as_('id'),
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
date_column,
where=(move.product == product_id)
where=product_clause
& (move.from_location.in_(warehouse_query)
| move.to_location.in_(warehouse_query))
& (Coalesce(move.effective_date, move.planned_date) != Null),
@@ -328,9 +342,26 @@ class ProductQuantitiesByWarehouse(ModelSQL, ModelView):
@classmethod
def get_quantity(cls, lines, name):
Product = Pool().get('product.product')
trans_context = Transaction().context
product_id = Transaction().context.get('product')
warehouse_id = Transaction().context.get('warehouse')
def valid_context(name):
return (trans_context.get(name) is not None
and isinstance(trans_context[name], (int, long)))
if not any(map(valid_context, ['product', 'product_template'])):
return {l.id: None for l in lines}
if trans_context.get('product') is not None:
product_ids = [trans_context['product']]
grouping = None
grouping_filter = None
key = trans_context['product']
else:
product_ids = None
grouping = ('product.template',)
grouping_filter = ([trans_context.get('product_template')],)
key = trans_context['product_template']
warehouse_id = trans_context.get('warehouse')
dates = sorted(l.date for l in lines)
quantities = {}
@@ -343,8 +374,11 @@ class ProductQuantitiesByWarehouse(ModelSQL, ModelView):
}
with Transaction().set_context(**context):
quantities[date] = Product.products_by_location(
[warehouse_id], [product_id],
with_childs=True).get((warehouse_id, product_id), 0)
location_ids=[warehouse_id],
product_ids=product_ids,
grouping=grouping,
grouping_filter=grouping_filter,
with_childs=True).get((warehouse_id, key), 0)
try:
date_start = date + datetime.timedelta(1)
except OverflowError:
+16 -4
View File
@@ -44,8 +44,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">Locations Quantity</field>
<field name="res_model">stock.location</field>
<field name="domain" eval="[('parent', '=', None)]" pyson="1"/>
<field name="context" eval="{'product': Eval('active_id')}"
pyson="1"/>
<field name="context" pyson="1"
eval="If(Eval('active_model') == 'product.template', {'product_template': Eval('active_id')}, {'product': Eval('active_id')})"/>
<field name="context_model">product.by_location.context</field>
</record>
<record model="ir.action.act_window.view" id="act_location_quantity_tree_view">
@@ -59,6 +59,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">product.product,-1</field>
<field name="action" ref="act_location_quantity_tree"/>
</record>
<record model="ir.action.keyword"
id="act_location_quantity_keyword2">
<field name="keyword">form_relate</field>
<field name="model">product.template,-1</field>
<field name="action" ref="act_location_quantity_tree"/>
</record>
<record model="ir.action-res.group"
id="act_location-quantity_tree-group_stock">
<field name="action" ref="act_location_quantity_tree"/>
@@ -87,8 +93,8 @@ this repository contains the full copyright notices and license terms. -->
id="act_product_quantities_warehouse">
<field name="name">Product Quantities By Warehouse</field>
<field name="res_model">stock.product_quantities_warehouse</field>
<field name="context" eval="{'product': Eval('active_id')}"
pyson="1"/>
<field name="context" pyson="1"
eval="If(Eval('active_model') == 'product.template', {'product_template': Eval('active_id')}, {'product': Eval('active_id')})"/>
<field name="search_value" eval="[('date', '>=', Date())]"
pyson="1"/>
<field name="context_model">stock.product_quantities_warehouse.context</field>
@@ -119,6 +125,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">product.product,-1</field>
<field name="action" ref="act_product_quantities_warehouse"/>
</record>
<record model="ir.action.keyword"
id="act_product_quantities_warehouse_keyword2">
<field name="keyword">form_relate</field>
<field name="model">product.template,-1</field>
<field name="action" ref="act_product_quantities_warehouse"/>
</record>
<record model="ir.action-res.group"
id="act_product_quantities_warehouse-group_stock">
<field name="action" ref="act_product_quantities_warehouse"/>
+71
View File
@@ -551,6 +551,77 @@ class StockTestCase(ModuleTestCase):
"Test products_by_location on flat_childs with period closed"
self.test_products_by_location_flat_childs(period_closed=True)
@with_transaction()
def test_templates_by_location(self, period_closed=False):
"Test products_by_location grouped by template"
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')
Period = pool.get('stock.period')
Date = pool.get('ir.date')
unit, = Uom.search([('name', '=', 'Unit')])
template, = Template.create([{
'name': 'Template',
'type': 'goods',
'list_price': Decimal(0),
'default_uom': unit.id,
}])
product1, product2 = Product.create([{
'template': template.id,
}] * 2)
lost_found, = Location.search([('type', '=', 'lost_found')])
storage, = Location.search([('code', '=', 'STO')])
input_, = Location.search([('code', '=', 'IN')])
company = create_company()
with set_company(company):
date = Date.today() - relativedelta(days=1)
moves = Move.create([{
'product': product1.id,
'uom': unit.id,
'quantity': 2,
'from_location': lost_found.id,
'to_location': storage.id,
'planned_date': date,
'effective_date': date,
'company': company.id,
}, {
'product': product2.id,
'uom': unit.id,
'quantity': 3,
'from_location': input_.id,
'to_location': storage.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])
templates_by_location = Product.products_by_location(
[storage.id],
grouping=('product.template',),
grouping_filter=([template.id],))
self.assertDictEqual(templates_by_location, {
(storage.id, template.id): 5,
})
def test_templates_by_location_period_closed(self):
"Test products_by_location grouped by template with period closed"
self.test_templates_by_location(period_closed=True)
@with_transaction()
def test_period(self):
'Test period'