Commit a121ee63 authored by Julien Muchembled's avatar Julien Muchembled

Type-based 'getBaseAmountQuantity' is given a "brain" wrapping the input amount

As first parameter, an object wrapping the input amount (by acquisition)
is passed instead of the amount itself.
The purpose of this object is to provide additional information about current
amount generation. It has 2 public methods:
- getGeneratedAmountQuantity
- getBaseAmountList

git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@39101 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent b4d6c89c
...@@ -64,6 +64,7 @@ class AmountGeneratorLine(MappedValue, XMLMatrix, Amount, ...@@ -64,6 +64,7 @@ class AmountGeneratorLine(MappedValue, XMLMatrix, Amount,
def getBaseAmountQuantity(cls, delivery_amount, base_application, rounding): def getBaseAmountQuantity(cls, delivery_amount, base_application, rounding):
"""Default method to compute quantity for the given base_application""" """Default method to compute quantity for the given base_application"""
value = delivery_amount.getGeneratedAmountQuantity(base_application) value = delivery_amount.getGeneratedAmountQuantity(base_application)
delivery_amount = delivery_amount.getObject()
if base_application in delivery_amount.getBaseContributionList(): if base_application in delivery_amount.getBaseContributionList():
value += cls._getBaseAmountQuantity(delivery_amount) value += cls._getBaseAmountQuantity(delivery_amount)
return value return value
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
import random import random
import zope.interface import zope.interface
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Acquisition import Implicit
from Products.ERP5Type import Permissions, interfaces from Products.ERP5Type import Permissions, interfaces
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5.Document.MappedValue import MappedValue from Products.ERP5.Document.MappedValue import MappedValue
...@@ -43,76 +44,84 @@ from Products.ERP5.Document.MappedValue import MappedValue ...@@ -43,76 +44,84 @@ from Products.ERP5.Document.MappedValue import MappedValue
# Old simulation implemented both but they conflict. # Old simulation implemented both but they conflict.
# Current code implements the 2nd option: Should we use 'use' instead ? # Current code implements the 2nd option: Should we use 'use' instead ?
class BaseAmount(dict): class BaseAmountDict(Implicit):
"""Dictionary holding accumulated base amounts """Dictionary holding accumulated base amounts
""" """
def __init__(self, cache, method_kw):
def __init__(self, context, cache, method_kw): self._dict = {}
self._context = context
self._frozen = set() self._frozen = set()
self._lazy = [] self._amount_list = []
self._cache = cache self._cache = cache
self._method_kw = method_kw self._method_kw = method_kw
def getContext(self):
return self._context
def setAmountGeneratorLine(self, amount_generator_line): def setAmountGeneratorLine(self, amount_generator_line):
self._amount_generator_line = amount_generator_line self._amount_generator_line = amount_generator_line
def recurse(self, portal_type=None): def recurseMovementList(self, movement_list):
for amount in self._context.objectValues(portal_type=portal_type): for amount in movement_list:
# Add only movement which are input. Output will be recalculated. # Add only movement which are input. Output will be recalculated.
# XXX See above comment about the absence of base_application # XXX See above comment about the absence of base_application
# (for example, we could check if resource use category is in the # (for example, we could check if resource use category is in the
# normal resource use preference list). # normal resource use preference list).
if not amount.getBaseApplication(): if not amount.getBaseApplication():
base_amount = self.__class__(amount, self._cache, self._method_kw) amount = self.__class__(self._cache, self._method_kw).__of__(amount)
self._lazy.append(base_amount) self._amount_list.append(amount)
for base_amount in base_amount.recurse(portal_type): yield amount
yield base_amount
yield self yield self
def __getitem__(self, key): def contribute(self, base_amount, value):
"""Get intermediate computed quantity for given base_application""" if base_amount in self._frozen:
if key in self._frozen:
raise ValueError("Can not contribute to %r because this base_amount is" raise ValueError("Can not contribute to %r because this base_amount is"
" already applied. Order of Amount Generator Lines is" " already applied. Order of Amount Generator Lines is"
" wrong." % key) " wrong." % base_amount)
self._dict[base_amount] = self._getQuantity(base_amount) + value
def _getQuantity(self, base_amount):
"""Get intermediate computed quantity for given base_application"""
try: try:
return dict.__getitem__(self, key) return self._dict[base_amount]
except KeyError: except KeyError:
value = 0 value = 0
amount_generator_line = self._amount_generator_line amount_generator_line = self._amount_generator_line
for lazy in self._lazy: for base_amount_dict in self._amount_list:
lazy._amount_generator_line = amount_generator_line base_amount_dict._amount_generator_line = amount_generator_line
value += lazy.getQuantity(key) value += base_amount_dict.getGeneratedAmountQuantity(base_amount)
self[key] = value self._dict[base_amount] = value
return value return value
def getQuantity(self, key): getBaseAmountList__roles__ = None # public
"""Get final computed quantity for given base_application def getBaseAmountList(self):
"""Return list of amounts that are sub-objects of self
Returned objects are wrapped like self.
Example: for a delivery, they are manually created movements.
"""
return list(self._amount_list)
getGeneratedAmountQuantity__roles__ = None # public
def getGeneratedAmountQuantity(self, base_amount):
"""Get final computed quantity for given base_amount
Note: During a call to getQuantity, this method may be called again by Note: During a call to getQuantity, this method may be called again by
getGeneratedAmountQuantity for the same amount and key. getGeneratedAmountQuantity for the same amount and key.
In this case, the returned value is the last intermediate value just In this case, the returned value is the last intermediate value just
before finalization. before finalization.
""" """
if key in self._frozen: if base_amount in self._frozen:
return dict.__getitem__(self, key) return self._getQuantity(base_amount)
self[key] # initialize entry before we freeze it self._frozen.add(base_amount)
self._frozen.add(key)
try: try:
method = self._cache[key] method = self._cache[base_amount]
except KeyError: except KeyError:
method = self._amount_generator_line._getTypeBasedMethod( method = self._amount_generator_line._getTypeBasedMethod(
'getBaseAmountQuantityMethod') 'getBaseAmountQuantityMethod')
if method is not None: if method is not None:
method = method(key) method = method(base_amount)
if method is None: if method is None:
method = self._amount_generator_line.getBaseAmountQuantity method = self._amount_generator_line.getBaseAmountQuantity
self._cache[key] = method self._cache[base_amount] = method
self[key] = value = method(self._context, key, **self._method_kw) value = method(self, base_amount, **self._method_kw)
self._dict[base_amount] = value
return value return value
...@@ -135,14 +144,6 @@ class AmountGeneratorMixin: ...@@ -135,14 +144,6 @@ class AmountGeneratorMixin:
# Declarative interfaces # Declarative interfaces
zope.interface.implements(interfaces.IAmountGenerator,) zope.interface.implements(interfaces.IAmountGenerator,)
security.declareProtected(Permissions.AccessContentsInformation,
'getGeneratedAmountQuantity')
def getGeneratedAmountQuantity(self, base_application):
"""Give access to computed quantities during generation of amounts"""
base_amount = getTransactionalVariable()[
'amount_generator.getGeneratedAmountList'][self]
return base_amount.getQuantity(base_application)
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'getGeneratedAmountList') 'getGeneratedAmountList')
def getGeneratedAmountList(self, amount_list=None, rounding=False, def getGeneratedAmountList(self, amount_list=None, rounding=False,
...@@ -169,23 +170,25 @@ class AmountGeneratorMixin: ...@@ -169,23 +170,25 @@ class AmountGeneratorMixin:
result = [] result = []
args = (getTransactionalVariable().setdefault( args = (getTransactionalVariable().setdefault(
"amount_generator.BaseAmount", {}), "amount_generator.BaseAmountDict", {}),
dict(rounding=rounding)) dict(rounding=rounding))
# If amount_list is None, then try to collect amount_list from # If amount_list is None, then try to collect amount_list from
# the current context # the current context
if amount_list is None: if amount_list is None:
if self.providesIMovementCollection(): if self.providesIMovementCollection():
base_amount_list = BaseAmount(self, *args) \ base_amount_list = BaseAmountDict(*args).__of__(self) \
.recurse(amount_generator_type_list) .recurseMovementList(self.getMovementList())
elif self.providesIAmount(): elif self.providesIAmount():
base_amount_list = BaseAmount(self, *args), base_amount_list = BaseAmountDict(*args).__of__(self),
elif self.providesIAmountList(): elif self.providesIAmountList():
base_amount_list = (BaseAmount(amount, *args) for amount in self) base_amount_list = (BaseAmountDict(*args).__of__(amount)
for amount in self)
else: else:
raise ValueError("%r must implement IMovementCollection, IAmount or" raise ValueError("%r must implement IMovementCollection, IAmount or"
" IAmountList" % self) " IAmountList" % self)
else: else:
base_amount_list = (BaseAmount(amount, *args) for amount in amount_list) base_amount_list = (BaseAmountDict(*args).__of__(amount)
for amount in amount_list)
# First define the method that will browse recursively # First define the method that will browse recursively
# the amount generator lines and accumulate applicable values # the amount generator lines and accumulate applicable values
...@@ -264,7 +267,7 @@ class AmountGeneratorMixin: ...@@ -264,7 +267,7 @@ class AmountGeneratorMixin:
# need values converted to the default management unit. # need values converted to the default management unit.
# If no quantity is provided, we consider that the value is 1.0 # If no quantity is provided, we consider that the value is 1.0
# (XXX is it OK ?) XXX-JPS Need careful review with taxes # (XXX is it OK ?) XXX-JPS Need careful review with taxes
quantity = float(sum(map(base_amount.getQuantity, quantity = float(sum(map(base_amount.getGeneratedAmountQuantity,
base_application_set))) base_application_set)))
for quantity_key in ('net_quantity', 'converted_quantity', for quantity_key in ('net_quantity', 'converted_quantity',
'net_converted_quantity', 'quantity'): 'net_converted_quantity', 'quantity'):
...@@ -297,25 +300,17 @@ class AmountGeneratorMixin: ...@@ -297,25 +300,17 @@ class AmountGeneratorMixin:
quantity *= (property_dict.get('price') or 1) / \ quantity *= (property_dict.get('price') or 1) / \
(property_dict.get('efficiency') or 1) (property_dict.get('efficiency') or 1)
for base_contribution in property_dict['base_contribution_set']: for base_contribution in property_dict['base_contribution_set']:
base_amount[base_contribution] += quantity base_amount.contribute(base_contribution, quantity)
is_mapped_value = isinstance(self, MappedValue) is_mapped_value = isinstance(self, MappedValue)
tv = getTransactionalVariable() for base_amount in base_amount_list:
# backup & restore existing cached value for reentrancy delivery_amount = base_amount.getObject()
original_cache = tv.get('amount_generator.getGeneratedAmountList') if not is_mapped_value:
try: self = delivery_amount.asComposedDocument(amount_generator_type_list)
tv['amount_generator.getGeneratedAmountList'] = base_amount_cache = {} # Browse recursively the amount generator lines and accumulate
for base_amount in base_amount_list: # applicable values - now execute the method
delivery_amount = base_amount.getContext() accumulateAmountList(self)
base_amount_cache[delivery_amount] = base_amount
if not is_mapped_value:
self = delivery_amount.asComposedDocument(amount_generator_type_list)
# Browse recursively the amount generator lines and accumulate
# applicable values - now execute the method
accumulateAmountList(self)
finally:
tv['amount_generator.getGeneratedAmountList'] = original_cache
return result return result
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
......
...@@ -170,7 +170,7 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -170,7 +170,7 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
def getBaseAmountQuantity(delivery_amount, base_application, **kw): def getBaseAmountQuantity(delivery_amount, base_application, **kw):
if delivery_amount.isDelivery(): if delivery_amount.isDelivery():
total_quantity = sum([movement.getQuantity() total_quantity = sum([movement.getQuantity()
for movement in delivery_amount.getMovementList() for movement in delivery_amount.getBaseAmountList()
if base_application in movement.getBaseContributionList()]) if base_application in movement.getBaseContributionList()])
if total_quantity < 3: if total_quantity < 3:
return 0 return 0
...@@ -230,7 +230,7 @@ return getBaseAmountQuantity""") ...@@ -230,7 +230,7 @@ return getBaseAmountQuantity""")
'special_discount', """\ 'special_discount', """\
return lambda delivery_amount, base_application, **kw: \\ return lambda delivery_amount, base_application, **kw: \\
3 <= sum([movement.getQuantity() 3 <= sum([movement.getQuantity()
for movement in delivery_amount.getMovementList() for movement in delivery_amount.getBaseAmountList()
if base_application in movement.getBaseContributionList()])""") if base_application in movement.getBaseContributionList()])""")
trade_condition = self.createTradeCondition( trade_condition = self.createTradeCondition(
...@@ -286,7 +286,7 @@ return lambda delivery_amount, base_application, **kw: \\ ...@@ -286,7 +286,7 @@ return lambda delivery_amount, base_application, **kw: \\
'special_discount', """\ 'special_discount', """\
def getBaseAmountQuantity(delivery_amount, base_application, **kw): def getBaseAmountQuantity(delivery_amount, base_application, **kw):
total_quantity = sum([movement.getQuantity() total_quantity = sum([movement.getQuantity()
for movement in delivery_amount.getMovementList() for movement in delivery_amount.getBaseAmountList()
if base_application in movement.getBaseContributionList()]) if base_application in movement.getBaseContributionList()])
if total_quantity < 3: if total_quantity < 3:
return 0 return 0
...@@ -421,7 +421,7 @@ return lambda delivery_amount, base_application, **kw: \\ ...@@ -421,7 +421,7 @@ return lambda delivery_amount, base_application, **kw: \\
'special_discount', """\ 'special_discount', """\
def getBaseAmountQuantity(delivery_amount, base_application, **kw): def getBaseAmountQuantity(delivery_amount, base_application, **kw):
highest_price = quantity = 0 highest_price = quantity = 0
for movement in delivery_amount.getMovementList(): for movement in delivery_amount.getBaseAmountList():
if base_application in movement.getBaseContributionList(): if base_application in movement.getBaseContributionList():
quantity += movement.getQuantity() quantity += movement.getQuantity()
if movement.getResourceValue().getProductLine() == 'video/dvd': if movement.getResourceValue().getProductLine() == 'video/dvd':
......
...@@ -504,6 +504,12 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -504,6 +504,12 @@ class TestTradeModelLine(TestTradeModelLineMixin):
self.tic() self.tic()
if not build: if not build:
# Check amount_generator refuses to produce amounts
# if lines are not ordered correctly.
self['trade_model_line/tax'].setIntIndex(0)
self.assertRaises(ValueError, order.getGeneratedAmountList)
transaction.abort()
for movement in (order, order['taxed'], order['discounted'], for movement in (order, order['taxed'], order['discounted'],
order['taxed_discounted']): order['taxed_discounted']):
self.checkComposition(movement, [trade_condition], { self.checkComposition(movement, [trade_condition], {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment