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
This diff is collapsed.
...@@ -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