Commit 73b325c5 authored by Julien Muchembled's avatar Julien Muchembled

Amount Generator: automatic sort based on application/contribution dependencies [2/2]

This implements dependency resolution to sort amount generator lines
so that a base_amount is never contributed after it was applied.

Before, it was required to sort manually using int_index or float_index, which
can be difficult for a human when there are many lines spreaded over different
containers (which are merged by composition). Another problematic case is when
a set of lines is configured by a user (like discounts & fees) and must all be
applied before other lines (taxes) that are installed elsewhere by the
developer: how to reliably make sure the latter have index values that are
already greater than those entered by the user ?

Setting int_index or float_index is now only useful for lines:
- with same reference: only the maching one with lowest index is taken
  into account (commit 68ec6bda)
- applying to intermediate values of some base_amount
  (commit 10be013b)

The difficult part to solve dependencies is that the calculation for a
given base_amount may trigger the application of other base_amount, and so on
recursively. In order to support this case, amount generator lines are first
applied on a dummy amount, and getGeneratedAmountQuantity must be call
unconditionally for all dependent base_amount. So optimizing like

  return 3 <= delivery_amount.getGeneratedAmountQuantity('base_amount/1') \
      or 1 <= delivery_amount.getGeneratedAmountQuantity('base_amount/2')

is wrong except if 'base_amount/2' is only contributed by the movement or if
you sort manually with indices.

Dependency resolution has precedence over indices. But since the sort is stable,
lines will remain sorted by index if it respects dependencies.
parent dd10a334
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
# #
############################################################################## ##############################################################################
from collections import defaultdict, deque
import random import random
import zope.interface import zope.interface
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
...@@ -82,8 +83,10 @@ class BaseAmountDict(Implicit): ...@@ -82,8 +83,10 @@ class BaseAmountDict(Implicit):
if variation_category_list: if variation_category_list:
base_amount = (base_amount,) + variation_category_list base_amount = (base_amount,) + variation_category_list
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. This should only happen in you have custom"
" wrong." % (base_amount,)) " calculation for some base_amount and your code does not call"
" getGeneratedAmountQuantity unconditionally for all base_amount"
" it depends on." % (base_amount,))
self._dict[variated_base_amount] = \ self._dict[variated_base_amount] = \
self._getQuantity(variated_base_amount) + value self._getQuantity(variated_base_amount) + value
...@@ -140,6 +143,11 @@ class BaseAmountDict(Implicit): ...@@ -140,6 +143,11 @@ class BaseAmountDict(Implicit):
if variated_base_amount in self._frozen: if variated_base_amount in self._frozen:
return self._getQuantity(variated_base_amount) return self._getQuantity(variated_base_amount)
self._frozen.add(variated_base_amount) self._frozen.add(variated_base_amount)
value = self._dict[variated_base_amount] = \
self._getGeneratedAmountQuantity(base_amount, variation_category_list)
return value
def _getGeneratedAmountQuantity(self, base_amount, variation_category_list):
try: try:
method = self._cache[base_amount] method = self._cache[base_amount]
except KeyError: except KeyError:
...@@ -155,9 +163,59 @@ class BaseAmountDict(Implicit): ...@@ -155,9 +163,59 @@ class BaseAmountDict(Implicit):
variation_category_list=variation_category_list) variation_category_list=variation_category_list)
else: else:
kw = self._method_kw kw = self._method_kw
value = method(self, base_amount, **kw) return method(self, base_amount, **kw)
self._dict[variated_base_amount] = value
return value
class BaseAmountResolver(BaseAmountDict):
_dummy_property_dict = {'_index': 0},
def __init__(self, cache, method_kw):
self._dict = cache.setdefault(None, {})
self._cache = cache
self._method_kw = method_kw
def __call__(self, delivery_amount, property_dict_list):
if property_dict_list:
recurseApplicationDependencies = \
self.__of__(delivery_amount).getGeneratedAmountQuantity
contribution_dict = defaultdict(list)
for property_dict in property_dict_list:
self._amount_generator_line = property_dict[None]
self._resolving = property_dict['_index'] = set()
for variated_base_amount in property_dict['_application']:
recurseApplicationDependencies(*variated_base_amount)
for variated_base_amount in property_dict['_contribution']:
contribution_dict[variated_base_amount].append(property_dict)
del self._resolving
contribution_dict.default_factory = lambda: self._dummy_property_dict
def sort_key(property_dict):
score = property_dict['_index']
if type(score) is set:
score = property_dict['_index'] = 1 + max(sort_key(x)
for x in score
for x in contribution_dict[x])
return score
property_dict_list.sort(key=sort_key)
for property_dict in property_dict_list:
del property_dict['_index']
def getBaseAmountList(self):
return ()
def _getQuantity(self, variated_base_amount):
return 0
def getGeneratedAmountQuantity(self, base_amount, variation_category_list=()):
variated_base_amount = base_amount, variation_category_list
resolving = self._resolving
if variated_base_amount not in self._dict:
self._resolving = self._dict[variated_base_amount] = set(
(variated_base_amount,))
self._getGeneratedAmountQuantity(base_amount, variation_category_list)
self._resolving = resolving
resolving |= self._dict[variated_base_amount]
return 0
class AmountGeneratorMixin: class AmountGeneratorMixin:
...@@ -233,36 +291,48 @@ class AmountGeneratorMixin: ...@@ -233,36 +291,48 @@ class AmountGeneratorMixin:
return (line.getFloatIndex() if int_index is None else int_index, return (line.getFloatIndex() if int_index is None else int_index,
random.random()) random.random())
def accumulateAmountList(self): is_mapped_value = isinstance(self, MappedValue)
"""Browse recursively the amount generator lines recurse_queue = deque()
and accumulate applicable values resolver = BaseAmountResolver(*args)
"""
if 1: for base_amount in base_amount_list:
amount_generator_line_list = self.contentValues( delivery_amount = base_amount.getObject()
recurse_queue.append(self if is_mapped_value
else delivery_amount.asComposedDocument(amount_generator_type_list))
property_dict_list = []
# If several amount generator lines have same reference, the first
# (sorted by int_index or float_index) matching one will mask the others.
reference_set = set()
while recurse_queue:
self = recurse_queue.popleft()
amount_generator_line_list = self.objectValues(
portal_type=amount_generator_line_type_list) portal_type=amount_generator_line_type_list)
# Recursively feed base_amount # Recursively feed base_amount
if amount_generator_line_list: if amount_generator_line_list:
# First sort so that a line can mask other of same reference.
# We will sort again later to satisfy dependencies between
# base_application & base_contribution.
amount_generator_line_list.sort(key=getLineSortKey) amount_generator_line_list.sort(key=getLineSortKey)
for amount_generator_line in amount_generator_line_list: recurse_queue += amount_generator_line_list
accumulateAmountList(amount_generator_line) continue
return if self.getPortalType() not in amount_generator_line_type_list:
elif (self.getPortalType() not in amount_generator_line_type_list): continue
return target_method = 'isDelivery' if self.isTargetDelivery() \
target_method = self.isTargetDelivery() and 'isDelivery' or default_target else default_target
if target_method and not getattr(delivery_amount, target_method)(): if target_method and not getattr(delivery_amount, target_method)():
return continue
if not self.test(delivery_amount): if not self.test(delivery_amount):
return continue
self = self.asPredicate() self = self.asPredicate()
reference = self.getReference() reference = self.getReference()
if reference: if reference:
if reference in reference_set: if reference in reference_set:
return continue
reference_set.add(reference) reference_set.add(reference)
# Try to collect cells and aggregate their mapped properties # Try to collect cells and aggregate their mapped properties
# using resource + variation as aggregation key or base_application # using resource + variation as aggregation key or base_application
# for intermediate lines. # for intermediate lines.
amount_generator_cell_list = [self] + self.contentValues( amount_generator_cell_list = [self] + self.objectValues(
portal_type=amount_generator_cell_type_list) portal_type=amount_generator_cell_type_list)
cell_aggregate = {} # aggregates final line information cell_aggregate = {} # aggregates final line information
...@@ -273,11 +343,12 @@ class AmountGeneratorMixin: ...@@ -273,11 +343,12 @@ class AmountGeneratorMixin:
if not cell.test(delivery_amount): if not cell.test(delivery_amount):
continue continue
cell = cell.asPredicate() cell = cell.asPredicate()
key = cell.getCellAggregateKey() aggregate_key = cell.getCellAggregateKey()
try: try:
property_dict = cell_aggregate[key] property_dict = cell_aggregate[aggregate_key]
except KeyError: except KeyError:
cell_aggregate[key] = property_dict = { cell_aggregate[aggregate_key] = property_dict = {
None: self,
'base_application_set': set(base_application_list), 'base_application_set': set(base_application_list),
'base_contribution_set': set(base_contribution_list), 'base_contribution_set': set(base_contribution_list),
'category_list': [], 'category_list': [],
...@@ -300,7 +371,9 @@ class AmountGeneratorMixin: ...@@ -300,7 +371,9 @@ class AmountGeneratorMixin:
cell.getMappedValueBaseCategoryList(), base=1) cell.getMappedValueBaseCategoryList(), base=1)
property_dict['category_list'] += category_list property_dict['category_list'] += category_list
property_dict['resource'] = cell.getResource() property_dict['resource'] = cell.getResource()
if cell is not self: if cell is self:
self_key = aggregate_key
else:
# cells inherit base_application and base_contribution from line # cells inherit base_application and base_contribution from line
property_dict['base_application_set'].update( property_dict['base_application_set'].update(
cell.getBaseApplicationList()) cell.getBaseApplicationList())
...@@ -308,8 +381,6 @@ class AmountGeneratorMixin: ...@@ -308,8 +381,6 @@ class AmountGeneratorMixin:
cell.getBaseContributionList()) cell.getBaseContributionList())
property_dict['causality_value_list'].append(cell) property_dict['causality_value_list'].append(cell)
base_amount.setAmountGeneratorLine(self)
for property_dict in cell_aggregate.itervalues():
# Ignore line (i.e. self) if cells produce unrelated amounts. # Ignore line (i.e. self) if cells produce unrelated amounts.
# With Transformed Resource (Transformation), line is considered in # With Transformed Resource (Transformation), line is considered in
# order to gather common properties and cells are used to describe # order to gather common properties and cells are used to describe
...@@ -317,23 +388,40 @@ class AmountGeneratorMixin: ...@@ -317,23 +388,40 @@ class AmountGeneratorMixin:
# In cases like trade, payroll or assorted resources, # In cases like trade, payroll or assorted resources,
# we want to ignore the line if they are cells. # we want to ignore the line if they are cells.
# See also implementations of 'getCellAggregateKey' # See also implementations of 'getCellAggregateKey'
causality_value = property_dict['causality_value_list'][-1] if len(cell_aggregate) > 1 and \
if causality_value is self and len(cell_aggregate) > 1: len(cell_aggregate[self_key]['causality_value_list']) == 1:
continue del cell_aggregate[self_key]
base_application_set = property_dict['base_application_set']
# allow a single base_application to be variated # Allow base_application & base_contribution to be variated.
variation_category_list = tuple(sorted([x for x in base_application_set for property_dict in cell_aggregate.itervalues():
if x[:12] != 'base_amount/'])) base_amount_set = property_dict['base_application_set']
if variation_category_list: variation_list = tuple(sorted(x for x in base_amount_set
base_application_set.difference_update(variation_category_list) if not x.startswith('base_amount/')))
base_amount_set.difference_update(variation_list)
# Before we ignored 'quantity=0' amount here for better performance, # Before we ignored 'quantity=0' amount here for better performance,
# but it makes expand unstable (e.g. when the first expand causes # but it makes expand unstable (e.g. when the first expand causes
# non-zero quantity and then quantity becomes zero). # non-zero quantity and then quantity becomes zero).
# Ignore only if there's no base_application. # Ignore only if there's no base_application.
if not base_application_set: if not base_amount_set:
continue continue
property_dict['_application'] = [(x, variation_list)
for x in base_amount_set]
base_amount_set = property_dict['base_contribution_set']
variation_list = tuple(sorted(x for x in base_amount_set
if not x.startswith('base_amount/')))
property_dict['_contribution'] = [(x, variation_list)
for x in base_amount_set.difference(variation_list)]
property_dict_list.append(property_dict)
# Sort amount generators according to
# base_application & base_contribution dependencies.
resolver(delivery_amount, property_dict_list)
# Accumulate applicable values.
for property_dict in property_dict_list:
self = property_dict.pop(None)
base_amount.setAmountGeneratorLine(self)
contribution_list = property_dict.pop('_contribution')
# property_dict may include # property_dict may include
# resource - VAT service or a Component in MRP # resource - VAT service or a Component in MRP
# (if unset, the amount will only be used for reporting) # (if unset, the amount will only be used for reporting)
...@@ -349,9 +437,8 @@ class AmountGeneratorMixin: ...@@ -349,9 +437,8 @@ class AmountGeneratorMixin:
# for future simulation of efficiencies. # for future simulation of efficiencies.
# 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(base_amount.getGeneratedAmountQuantity( quantity = float(sum(base_amount.getGeneratedAmountQuantity(*x)
base_application, variation_category_list) for x in property_dict.pop('_application')))
for base_application in base_application_set))
for key in 'quantity', 'price', 'efficiency': for key in 'quantity', 'price', 'efficiency':
if property_dict.get(key, 0) in (None, ''): if property_dict.get(key, 0) in (None, ''):
del property_dict[key] del property_dict[key]
...@@ -363,7 +450,8 @@ class AmountGeneratorMixin: ...@@ -363,7 +450,8 @@ class AmountGeneratorMixin:
# Create an Amount object # Create an Amount object
amount = newTempAmount(portal, amount = newTempAmount(portal,
# we only want the id to be unique so we pick a random causality # we only want the id to be unique so we pick a random causality
causality_value.getRelativeUrl().replace('/', '_'), property_dict['causality_value_list'][-1]
.getRelativeUrl().replace('/', '_'),
notify_workflow=False) notify_workflow=False)
amount._setCategoryList(property_dict.pop('category_list', ())) amount._setCategoryList(property_dict.pop('category_list', ()))
if amount.getQuantityUnit(): if amount.getQuantityUnit():
...@@ -388,26 +476,10 @@ class AmountGeneratorMixin: ...@@ -388,26 +476,10 @@ class AmountGeneratorMixin:
quantity /= property_dict.get('efficiency', 1) quantity /= property_dict.get('efficiency', 1)
except ZeroDivisionError: except ZeroDivisionError:
quantity *= float('inf') quantity *= float('inf')
base_contribution_set = property_dict['base_contribution_set'] for base_contribution, variation_category_list in contribution_list:
# allow a single base_contribution to be variated
variation_category_list = tuple(sorted([x for x in base_contribution_set
if x[:12] != 'base_amount/']))
if variation_category_list:
base_contribution_set.difference_update(variation_category_list)
for base_contribution in base_contribution_set:
base_amount.contribute(base_contribution, variation_category_list, base_amount.contribute(base_contribution, variation_category_list,
quantity) quantity)
is_mapped_value = isinstance(self, MappedValue)
for base_amount in base_amount_list:
delivery_amount = base_amount.getObject()
if not is_mapped_value:
self = delivery_amount.asComposedDocument(amount_generator_type_list)
# If several amount generator lines have same reference, the first
# (sorted by int_index or float_index) matching one will mask the others.
reference_set = set()
accumulateAmountList(self)
return result return result
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
......
...@@ -100,7 +100,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -100,7 +100,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
dict(title='Total Price Without VAT', dict(title='Total Price Without VAT',
reference='TOTAL_PRICE_WITHOUT_VAT', reference='TOTAL_PRICE_WITHOUT_VAT',
price=1, price=1,
int_index=10,
target_delivery=True, target_delivery=True,
base_application_list=('base_amount/discount_amount_of_non_vat_taxable', base_application_list=('base_amount/discount_amount_of_non_vat_taxable',
'base_amount/discount_amount_of_vat_taxable', 'base_amount/discount_amount_of_vat_taxable',
...@@ -110,7 +109,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -110,7 +109,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
dict(title='Total Price Of VAT Taxable', dict(title='Total Price Of VAT Taxable',
reference='TOTAL_PRICE_OF_VAT_TAXABLE', reference='TOTAL_PRICE_OF_VAT_TAXABLE',
price=1, price=1,
int_index=10,
target_delivery=True, target_delivery=True,
base_application_list=('base_amount/discount_amount_of_vat_taxable', base_application_list=('base_amount/discount_amount_of_vat_taxable',
'base_amount/vat_taxable'), 'base_amount/vat_taxable'),
...@@ -120,7 +118,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -120,7 +118,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
resource_value=self.service_discount, resource_value=self.service_discount,
price=1, price=1,
trade_phase='default/invoicing', trade_phase='default/invoicing',
int_index=10,
target_delivery=True, target_delivery=True,
base_application_list=('base_amount/discount_amount_of_vat_taxable', base_application_list=('base_amount/discount_amount_of_vat_taxable',
'base_amount/discount_amount_of_non_vat_taxable'), 'base_amount/discount_amount_of_non_vat_taxable'),
...@@ -130,7 +127,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -130,7 +127,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
resource_value=self.service_vat, resource_value=self.service_vat,
price=0.05, price=0.05,
trade_phase='default/invoicing', trade_phase='default/invoicing',
int_index=10,
target_delivery=True, target_delivery=True,
base_application_list=('base_amount/discount_amount_of_vat_taxable', base_application_list=('base_amount/discount_amount_of_vat_taxable',
'base_amount/vat_taxable'), 'base_amount/vat_taxable'),
...@@ -138,7 +134,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin): ...@@ -138,7 +134,6 @@ class TestComplexTradeModelLineUseCase(TestTradeModelLineMixin):
dict(title='Total Price With VAT', dict(title='Total Price With VAT',
reference='TOTAL_PRICE_WITH_VAT', reference='TOTAL_PRICE_WITH_VAT',
price=1, price=1,
int_index=20,
target_delivery=True, target_delivery=True,
base_application_list=('base_amount/vat_amount', base_application_list=('base_amount/vat_amount',
'base_amount/total_price_without_vat'), 'base_amount/total_price_without_vat'),
...@@ -182,7 +177,6 @@ return getBaseAmountQuantity""") ...@@ -182,7 +177,6 @@ return getBaseAmountQuantity""")
dict(reference='SPECIAL_DISCOUNT_3CD_LINEAR', dict(reference='SPECIAL_DISCOUNT_3CD_LINEAR',
resource_value=self.service_discount, resource_value=self.service_discount,
price=-0.1, price=-0.1,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=special_discount, base_application=special_discount,
base_contribution='base_amount/discount_amount_of_vat_taxable'), base_contribution='base_amount/discount_amount_of_vat_taxable'),
...@@ -238,7 +232,6 @@ return lambda delivery_amount, base_application, **kw: \\ ...@@ -238,7 +232,6 @@ return lambda delivery_amount, base_application, **kw: \\
resource_value=self.service_discount, resource_value=self.service_discount,
price=-1, price=-1,
quantity=500, quantity=500,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=special_discount, base_application=special_discount,
base_contribution='base_amount/discount_amount_of_vat_taxable'), base_contribution='base_amount/discount_amount_of_vat_taxable'),
...@@ -289,6 +282,9 @@ def getBaseAmountQuantity(delivery_amount, base_application, **kw): ...@@ -289,6 +282,9 @@ def getBaseAmountQuantity(delivery_amount, base_application, **kw):
if base_application in movement.getBaseContributionList()]) if base_application in movement.getBaseContributionList()])
if total_quantity < 3: if total_quantity < 3:
return 0 return 0
# Following expression should be evaluated during dependency resolution
# but it's ok to optimize here when total_quantity < 3 because
# 'total_price_of_ordered_items' is only contributed by movements.
return delivery_amount.getGeneratedAmountQuantity( return delivery_amount.getGeneratedAmountQuantity(
'base_amount/total_price_of_ordered_items') 'base_amount/total_price_of_ordered_items')
return getBaseAmountQuantity""") return getBaseAmountQuantity""")
...@@ -298,7 +294,6 @@ return getBaseAmountQuantity""") ...@@ -298,7 +294,6 @@ return getBaseAmountQuantity""")
dict(reference='SPECIAL_DISCOUNT_3CD_LINEAR', dict(reference='SPECIAL_DISCOUNT_3CD_LINEAR',
resource_value=self.service_discount, resource_value=self.service_discount,
price=-0.1, price=-0.1,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=special_discount, base_application=special_discount,
base_contribution='base_amount/discount_amount_of_vat_taxable'), base_contribution='base_amount/discount_amount_of_vat_taxable'),
...@@ -352,17 +347,19 @@ return getBaseAmountQuantity""" ...@@ -352,17 +347,19 @@ return getBaseAmountQuantity"""
'poster_present_3cd', total_quantity) 'poster_present_3cd', total_quantity)
special_discount = self.setBaseAmountQuantityMethod( special_discount = self.setBaseAmountQuantityMethod(
'special_discount', """\ 'special_discount', """\
return lambda delivery_amount, base_application, **kw: \\ def getBaseAmountQuantity(delivery_amount, base_application, **kw):
3 <= delivery_amount.getGeneratedAmountQuantity(%r) or \\ # Compute B even if A < 3 for dependency resolution.
1 <= delivery_amount.getGeneratedAmountQuantity(%r)""" # But like in test_usecase3, we could optimize.
% (poster_present_3cd, poster_present_1dvd)) A = delivery_amount.getGeneratedAmountQuantity(%r)
B = delivery_amount.getGeneratedAmountQuantity(%r)
return 3 <= A or 1 <= B
return getBaseAmountQuantity""" % (poster_present_3cd, poster_present_1dvd))
trade_condition = self.createTradeCondition( trade_condition = self.createTradeCondition(
self.trade_condition, ( self.trade_condition, (
dict(reference='SPECIAL_DISCOUNT_3CD_OR_1DVD_FIXED', dict(reference='SPECIAL_DISCOUNT_3CD_OR_1DVD_FIXED',
resource_value=self.poster, resource_value=self.poster,
price=0, price=0,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=special_discount), base_application=special_discount),
)) ))
...@@ -433,7 +430,6 @@ return getBaseAmountQuantity""") ...@@ -433,7 +430,6 @@ return getBaseAmountQuantity""")
dict(reference='SPECIAL_DISCOUNT_3CD', dict(reference='SPECIAL_DISCOUNT_3CD',
resource_value=self.service_discount, resource_value=self.service_discount,
price=-0.15, price=-0.15,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=special_discount, base_application=special_discount,
base_contribution='base_amount/discount_amount_of_vat_taxable'), base_contribution='base_amount/discount_amount_of_vat_taxable'),
...@@ -476,7 +472,6 @@ return lambda *args, **kw: 1""") ...@@ -476,7 +472,6 @@ return lambda *args, **kw: 1""")
dict(reference='SHIPPING_FEE', dict(reference='SHIPPING_FEE',
resource_value=self.service_discount, resource_value=self.service_discount,
quantity=500, quantity=500,
int_index=0,
target_delivery=True, target_delivery=True,
base_application=fixed_quantity, base_application=fixed_quantity,
base_contribution_list=('base_amount/additional_charge', base_contribution_list=('base_amount/additional_charge',
......
...@@ -1187,7 +1187,6 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase): ...@@ -1187,7 +1187,6 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase):
model = sequence.get('model') model = sequence.get('model')
model_line = self.createModelLine(model) model_line = self.createModelLine(model)
model_line.edit(title='intermediate line', model_line.edit(title='intermediate line',
int_index = 10,
reference='intermediate_line', reference='intermediate_line',
price=0.2, price=0.2,
base_contribution_list=['base_amount/payroll/base/income_tax'], base_contribution_list=['base_amount/payroll/base/income_tax'],
...@@ -1201,7 +1200,6 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase): ...@@ -1201,7 +1200,6 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase):
model = sequence.get('model') model = sequence.get('model')
model_line = self.createModelLine(model) model_line = self.createModelLine(model)
model_line.edit(title='line applied on intermediate line', model_line.edit(title='line applied on intermediate line',
int_index = 50,
trade_phase='payroll/france/urssaf', trade_phase='payroll/france/urssaf',
resource_value=sequence.get('urssaf_service'), resource_value=sequence.get('urssaf_service'),
reference='line_applied_on_intermediate_line', reference='line_applied_on_intermediate_line',
...@@ -1928,7 +1926,7 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase): ...@@ -1928,7 +1926,7 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase):
sequence=None, **kw): sequence=None, **kw):
model = sequence.get('model') model = sequence.get('model')
paysheet = sequence.get('paysheet') paysheet = sequence.get('paysheet')
property_list = ('title', 'description', 'int_index') property_list = 'title', 'description'
for model_line in model.contentValues(portal_type='Pay Sheet Model Line'): for model_line in model.contentValues(portal_type='Pay Sheet Model Line'):
model_line_resource = model_line.getResource() model_line_resource = model_line.getResource()
line_found = False line_found = False
...@@ -1949,12 +1947,10 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase): ...@@ -1949,12 +1947,10 @@ class TestPayrollMixin(TestTradeModelLineMixin, ERP5ReportTestCase):
def stepSetProperiesOnModelLines(self, sequence=None, **kw): def stepSetProperiesOnModelLines(self, sequence=None, **kw):
model = sequence.get('model') model = sequence.get('model')
index = 0 for index, model_line in enumerate(model.contentValues(
for model_line in model.contentValues(portal_type='Pay Sheet Model Line'): portal_type='Pay Sheet Model Line')):
model_line.setTitle('Model line title %s' % index) model_line.setTitle('Model line title %s' % index)
model_line.setDescription('Model line description %s' % index) model_line.setDescription('Model line description %s' % index)
model_line.setIntIndex(index)
index += 1
def checkPrecisionOfListBox(self, report_section, precision): def checkPrecisionOfListBox(self, report_section, precision):
here = report_section.getObject(self.portal) here = report_section.getObject(self.portal)
...@@ -3421,8 +3417,8 @@ class TestPayroll(TestPayrollMixin): ...@@ -3421,8 +3417,8 @@ class TestPayroll(TestPayrollMixin):
sequence_list.play(self) sequence_list.play(self)
def test_propertiesAreSetOnPaysheetLines(self): def test_propertiesAreSetOnPaysheetLines(self):
'''check porperties from model line (like description, int_index, '''check properties from model line (like description, title, ...)
title, ...) are copied on the paysheet lines''' are copied on the paysheet lines'''
sequence_list = SequenceList() sequence_list = SequenceList()
sequence_string = self.COMMON_BASIC_DOCUMENT_CREATION_SEQUENCE_STRING + """ sequence_string = self.COMMON_BASIC_DOCUMENT_CREATION_SEQUENCE_STRING + """
SetProperiesOnModelLines SetProperiesOnModelLines
......
...@@ -172,9 +172,7 @@ class TestTradeModelLineMixin(TestBPMMixin, UserDict): ...@@ -172,9 +172,7 @@ class TestTradeModelLineMixin(TestBPMMixin, UserDict):
title=self.id(), title=self.id(),
specialise_value_list=specialise_value_list, specialise_value_list=specialise_value_list,
**kw) **kw)
for int_index, line_kw in enumerate(trade_model_line_list): for kw in trade_model_line_list:
kw = dict(int_index=int_index)
kw.update(line_kw)
self.createTradeModelLine(trade_condition, **kw) self.createTradeModelLine(trade_condition, **kw)
return trade_condition return trade_condition
...@@ -509,14 +507,12 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -509,14 +507,12 @@ class TestTradeModelLine(TestTradeModelLineMixin):
base_contribution='base_amount/tax', base_contribution='base_amount/tax',
trade_phase='default/discount', trade_phase='default/discount',
resource_value=self.createServiceDiscount(), resource_value=self.createServiceDiscount(),
reference='discount', reference='discount'),
int_index=10),
dict(price=self.default_tax_ratio, dict(price=self.default_tax_ratio,
base_application='base_amount/tax', base_application='base_amount/tax',
trade_phase='default/tax', trade_phase='default/tax',
resource_value=self.createServiceTax(), resource_value=self.createServiceTax(),
reference='tax', reference='tax'),
int_index=20),
)) ))
order = self.createOrder(trade_condition, ( order = self.createOrder(trade_condition, (
dict(price=1, quantity=2, id='taxed', dict(price=1, quantity=2, id='taxed',
...@@ -540,12 +536,6 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -540,12 +536,6 @@ 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)
self.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], {
...@@ -729,35 +719,30 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -729,35 +719,30 @@ class TestTradeModelLine(TestTradeModelLineMixin):
base_contribution='base_amount/total_tax', base_contribution='base_amount/total_tax',
trade_phase='default/tax', trade_phase='default/tax',
resource_value=service_tax, resource_value=service_tax,
reference='service_tax', reference='service_tax'),
int_index=10),
dict(price=0.32, dict(price=0.32,
base_application='base_amount/discount', base_application='base_amount/discount',
base_contribution='base_amount/total_discount', base_contribution='base_amount/total_discount',
trade_phase='default/discount', trade_phase='default/discount',
resource_value=service_discount, resource_value=service_discount,
reference='total_dicount_2', reference='total_dicount_2'),
int_index=10),
dict(price=0.2, dict(price=0.2,
base_application='base_amount/tax', base_application='base_amount/tax',
base_contribution='base_amount/total_tax', base_contribution='base_amount/total_tax',
trade_phase='default/tax', trade_phase='default/tax',
resource_value=service_tax, resource_value=service_tax,
reference='service_tax_2', reference='service_tax_2'),
int_index=10),
dict(price=0.12, dict(price=0.12,
base_application='base_amount/total_tax', base_application='base_amount/total_tax',
base_contribution='base_amount/total_discount', base_contribution='base_amount/total_discount',
trade_phase='default/tax', trade_phase='default/tax',
resource_value=service_tax, resource_value=service_tax,
reference='tax_3', reference='tax_3'),
int_index=20),
dict(price=0.8, dict(price=0.8,
base_application='base_amount/total_discount', base_application='base_amount/total_discount',
trade_phase='default/discount', trade_phase='default/discount',
resource_value=service_discount, resource_value=service_discount,
reference='total_discount', reference='total_discount'),
int_index=30),
] ]
random.shuffle(line_list) random.shuffle(line_list)
trade_condition = self.createTradeCondition(business_process, line_list) trade_condition = self.createTradeCondition(business_process, line_list)
...@@ -812,16 +797,13 @@ return getBaseAmountQuantity""") ...@@ -812,16 +797,13 @@ return getBaseAmountQuantity""")
trade_condition = self.createTradeCondition(business_process, ( trade_condition = self.createTradeCondition(business_process, (
dict(price=0.3, dict(price=0.3,
base_application=base_amount, base_application=base_amount,
reference='tax1', reference='tax1'),
int_index=10),
dict(base_application=base_amount, dict(base_application=base_amount,
base_contribution='base_amount/total_tax', base_contribution='base_amount/total_tax',
reference='tax2', reference='tax2'),
int_index=20),
dict(base_application='base_amount/total_tax', dict(base_application='base_amount/total_tax',
base_contribution='base_amount/total', base_contribution='base_amount/total',
reference='tax3', reference='tax3'),
int_index=30),
)) ))
def createCells(line, matrix, base_application=(), base_contribution=()): def createCells(line, matrix, base_application=(), base_contribution=()):
range_list = [set() for x in iter(matrix).next()] range_list = [set() for x in iter(matrix).next()]
...@@ -899,6 +881,37 @@ return context""" % (base_amount, base_amount)) ...@@ -899,6 +881,37 @@ return context""" % (base_amount, base_amount))
self.assertAlmostEqual(total_price * total_ratio, self.assertAlmostEqual(total_price * total_ratio,
sum((x.getTotalPrice() for x in amount_list), total_price)) sum((x.getTotalPrice() for x in amount_list), total_price))
def test_05_dependencyResolution(self):
from Products.ERP5Type.Document import newTempAmount, newTempTradeModelLine
from Products.ERP5.mixin.amount_generator import BaseAmountResolver
resolver = BaseAmountResolver({}, {})
trade_model_line = newTempTradeModelLine(self.portal, '')
trade_model_line.getBaseAmountQuantity = \
lambda delivery_amount, base_amount: sum(map(
delivery_amount.getGeneratedAmountQuantity,
application_dict.get(base_amount, base_amount)))
application_dict = dict(B='bf', C='c', E='Bef')
property_dict_list = [{
None: trade_model_line,
'index': index,
'_application': [(x, ()) for x in application],
'_contribution': [(x, ()) for x in contribution],
} for index, application, contribution in (
(2, 'C', 'e'),
(3, 'dE', ''),
(0, 'a', 'b'),
(1, 'B', 'cd'),
)]
delivery_amount = newTempAmount(self.portal, '')
resolver(delivery_amount, property_dict_list)
self.assertEqual(range(len(property_dict_list)),
[x['index'] for x in property_dict_list])
# Retry with cache already filled.
property_dict_list.reverse()
resolver(delivery_amount, property_dict_list)
self.assertEqual(range(len(property_dict_list)),
[x['index'] for x in property_dict_list])
def test_tradeModelLineWithFixedPrice(self): def test_tradeModelLineWithFixedPrice(self):
""" """
Check it's possible to have fixed quantity on lines. Sometimes we want Check it's possible to have fixed quantity on lines. Sometimes we want
......
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