Add Mixin class with helper to setup stock quantity fields

Add StockMixin class in move.py file with helper functions to be used in getter
and searcher methods of quantity fields.
Reimplemented getter and searcher of quantity/forecast_quantity fields of
Product to use these helper functions.
Add test over product's quantity fields.

issue3279
review963003
This commit is contained in:
Guillem Barba
2013-09-09 14:11:53 +02:00
parent 4f833ee812
commit 0096fed655
4 changed files with 141 additions and 76 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add Mixin class with helper to setup stock quantity fields
* Allow to customize move creation of Inventory
* Allow to customize unique constraint on Inventory
* Allow specific grouping Period Cache
+118 -1
View File
@@ -1,5 +1,7 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import datetime
import operator
from decimal import Decimal
from functools import reduce, partial
from sql import Column
@@ -11,7 +13,7 @@ from trytond.pyson import In, Eval, Not, Equal, If, Get, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool
__all__ = ['Move']
__all__ = ['StockMixin', 'Move']
STATES = {
'readonly': In(Eval('state'), ['cancel', 'assigned', 'done']),
@@ -19,6 +21,121 @@ STATES = {
DEPENDS = ['state']
class StockMixin:
'''Mixin class with helper to setup stock quantity field.'''
@classmethod
def _quantity_context(cls, name):
pool = Pool()
Date = pool.get('ir.date')
context = Transaction().context
new_context = {}
if name == 'quantity':
if (context.get('stock_date_end')
and context['stock_date_end'] > Date.today()):
new_context['stock_date_end'] = Date.today()
elif name == 'forecast_quantity':
new_context['forecast'] = True
if not context.get('stock_date_end'):
new_context['stock_date_end'] = datetime.date.max
return new_context
@classmethod
def _get_quantity(cls, records, name, location_ids, products=None,
grouping=('product',), position=-1):
"""
Compute for each record the stock quantity in the default uom of the
product.
location_ids is the list of IDs of locations to take account to compute
the stock. It can't be empty.
products restrict the stock computation to the this products (more
efficient), so it should be the products related to records.
If it is None all products are used.
grouping defines how stock moves are grouped.
position defines which field of grouping corresponds to the record
whose quantity is computed.
Return a dictionary with records id as key and quantity as value.
"""
pool = Pool()
Product = pool.get('product.product')
record_ids = [r.id for r in records]
quantities = dict.fromkeys(record_ids, 0.0)
if not location_ids:
return quantities
product_ids = products and [p.id for p in products] or None
with Transaction().set_context(cls._quantity_context(name)):
pbl = Product.products_by_location(location_ids=location_ids,
product_ids=product_ids, with_childs=True,
grouping=grouping)
for key, quantity in pbl.iteritems():
# pbl could return None in some keys
if (key[position] is not None and
key[position] in quantities):
quantities[key[position]] += quantity
return quantities
@staticmethod
@classmethod
def _search_quantity(cls, name, location_ids, domain=None,
grouping=('product',), position=-1):
"""
Compute the domain to filter records which validates the domain over
quantity field.
location_ids is the list of IDs of locations to take account to compute
the stock. It can't be empty.
grouping defines how stock moves are grouped.
position defines which field of grouping corresponds to the record
whose quantity is computed.
"""
pool = Pool()
Product = pool.get('product.product')
if not location_ids or not domain:
return []
def _search_quantity_eval_domain(line, domain):
operator_funcs = {
'=': operator.eq,
'>=': operator.ge,
'>': operator.gt,
'<=': operator.le,
'<': operator.lt,
'!=': operator.ne,
'in': lambda v, l: v in l,
'not in': lambda v, l: v not in l,
}
field, op, operand = domain
value = line.get(field)
return operator_funcs[op](value, operand)
with Transaction().set_context(cls._quantity_context(name)):
pbl = Product.products_by_location(
location_ids=location_ids,
with_childs=True, grouping=grouping)
processed_lines = []
for key, quantity in pbl.iteritems():
# pbl could return None in some keys
if key[position] is not None:
processed_lines.append({
'record_id': key[position],
name: quantity,
})
record_ids = [line['record_id'] for line in processed_lines
if _search_quantity_eval_domain(line, domain)]
return [('id', 'in', record_ids)]
class Move(Workflow, ModelSQL, ModelView):
"Stock Move"
__name__ = 'stock.move'
+9 -73
View File
@@ -12,9 +12,11 @@ from trytond.model import ModelSQL, ModelView, fields
from trytond.wizard import Wizard, StateView, StateAction, Button
from trytond.pyson import PYSONEncoder, Eval, Or
from trytond.transaction import Transaction
from trytond.tools import safe_eval, reduce_ids
from trytond.tools import reduce_ids
from trytond.pool import Pool, PoolMeta
from .move import StockMixin
__all__ = ['Template', 'Product',
'ProductByLocationStart', 'ProductByLocation',
'ProductQuantitiesByWarehouse', 'ProductQuantitiesByWarehouseStart',
@@ -75,7 +77,8 @@ class Template:
super(Template, cls).write(templates, vals)
class Product:
class Product(object, StockMixin):
__metaclass__ = PoolMeta
__name__ = "product.product"
quantity = fields.Function(fields.Float('Quantity'), 'get_quantity',
searcher='search_quantity')
@@ -86,80 +89,13 @@ class Product:
@classmethod
def get_quantity(cls, products, name):
Date = Pool().get('ir.date')
quantities = dict((p.id, 0.0) for p in products)
if not Transaction().context.get('locations'):
return quantities
context = {}
if (name == 'quantity'
and Transaction().context.get('stock_date_end')
and Transaction().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'):
context['stock_date_end'] = datetime.date.max
with Transaction().set_context(context):
pbl = cls.products_by_location(
location_ids=Transaction().context['locations'],
product_ids=quantities.keys(), with_childs=True)
for location in Transaction().context['locations']:
for product in products:
quantities[product.id] += pbl.get((location, product.id), 0.0)
return quantities
@staticmethod
def _search_quantity_eval_domain(line, domain):
field, operator, operand = domain
value = line.get(field)
if value is None:
return False
if operator not in ("=", ">=", "<=", ">", "<", "!="):
return False
if operator == "=":
operator = "=="
return (safe_eval(str(value) + operator + str(operand)))
location_ids = Transaction().context.get('locations')
return cls._get_quantity(products, name, location_ids, products)
@classmethod
def search_quantity(cls, name, domain=None):
Date = Pool().get('ir.date')
if not (Transaction().context.get('locations') and domain):
return []
context = {}
if (name == 'quantity'
and Transaction().context.get('stock_date_end')
and Transaction().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'):
context['stock_date_end'] = datetime.date.max
with Transaction().set_context(context):
pbl = cls.products_by_location(
location_ids=Transaction().context['locations'],
with_childs=True).iteritems()
processed_lines = []
for (location, product), quantity in pbl:
processed_lines.append({
'location': location, # XXX useful ?
'product': product,
name: quantity,
})
res = [line['product'] for line in processed_lines
if cls._search_quantity_eval_domain(line, domain)]
return [('id', 'in', res)]
location_ids = Transaction().context.get('locations')
return cls._search_quantity(name, location_ids, domain)
@classmethod
def get_cost_value(cls, products, name):
+13 -2
View File
@@ -266,6 +266,17 @@ class StockTestCase(unittest.TestCase):
}, 6),
]
def tests_product_quantity(context, quantity):
with transaction.set_context(locations=[storage.id]):
product_reloaded = self.product(product.id)
if (not context.get('stock_date_end')
or context['stock_date_end'] > today
or context.get('forecast')):
self.assertEqual(product_reloaded.forecast_quantity,
quantity)
else:
self.assertEqual(product_reloaded.quantity, quantity)
def test_products_by_location():
for context, quantity in tests:
with transaction.set_context(context):
@@ -274,6 +285,7 @@ class StockTestCase(unittest.TestCase):
else:
self.assertEqual(products_by_location(),
{(storage.id, product.id): quantity})
tests_product_quantity(context, quantity)
test_products_by_location()
@@ -283,7 +295,7 @@ class StockTestCase(unittest.TestCase):
today + relativedelta(days=-4),
today + relativedelta(days=-3),
today + relativedelta(days=-2),
]
]
moves = self.move.create([{
'product': product.id,
@@ -450,7 +462,6 @@ class StockTestCase(unittest.TestCase):
'currency': currency.id,
}])
tests = [
(-5, {
supplier.id: -10,