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:
1 parent
ae3358481d
commit
55082d76a6
7 files changed
+238
-55
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"/>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user