Commit 99048a64 authored by Julien Muchembled's avatar Julien Muchembled

amount_generator: add support for variation

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@43576 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 5575028a
...@@ -56,17 +56,25 @@ class AmountGeneratorLine(MappedValue, XMLMatrix, Amount, ...@@ -56,17 +56,25 @@ class AmountGeneratorLine(MappedValue, XMLMatrix, Amount,
'getCellAggregateKey') 'getCellAggregateKey')
def getCellAggregateKey(self): def getCellAggregateKey(self):
"""Define a key in order to aggregate amounts at cell level""" """Define a key in order to aggregate amounts at cell level"""
return (self.getResource(), resource = self.getResource()
self.getVariationText()) # Variation UID, Hash ? if resource:
return (resource, self.getVariationText()) # Variation UID, Hash ?
# For a pure intermediate line, we need another way to prevent merging:
# do not merge if base_application or base_contribution is variated.
return frozenset(self.getBaseApplicationList() +
self.getBaseContributionList())
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'getBaseAmountQuantity') 'getBaseAmountQuantity')
@classmethod @classmethod
def getBaseAmountQuantity(cls, delivery_amount, base_application, rounding): def getBaseAmountQuantity(cls, delivery_amount, base_application,
variation_category_list=(), **kw):
"""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, variation_category_list)
delivery_amount = delivery_amount.getObject() delivery_amount = delivery_amount.getObject()
if base_application in delivery_amount.getBaseContributionList(): if base_application in delivery_amount.getBaseContributionList():
assert not variation_category_list
value += cls._getBaseAmountQuantity(delivery_amount) value += cls._getBaseAmountQuantity(delivery_amount)
return value return value
......
...@@ -70,24 +70,29 @@ class BaseAmountDict(Implicit): ...@@ -70,24 +70,29 @@ class BaseAmountDict(Implicit):
yield amount yield amount
yield self yield self
def contribute(self, base_amount, value): def contribute(self, base_amount, variation_category_list, value):
if base_amount in self._frozen: variated_base_amount = base_amount, variation_category_list
if variated_base_amount in self._frozen:
if 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. Order of Amount Generator Lines is"
" wrong." % base_amount) " wrong." % (base_amount,))
self._dict[base_amount] = self._getQuantity(base_amount) + value self._dict[variated_base_amount] = \
self._getQuantity(variated_base_amount) + value
def _getQuantity(self, base_amount): def _getQuantity(self, variated_base_amount):
"""Get intermediate computed quantity for given base_application""" """Get intermediate computed quantity for given base_application"""
try: try:
return self._dict[base_amount] return self._dict[variated_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 base_amount_dict in self._amount_list: for base_amount_dict in self._amount_list:
base_amount_dict._amount_generator_line = amount_generator_line base_amount_dict._amount_generator_line = amount_generator_line
value += base_amount_dict.getGeneratedAmountQuantity(base_amount) value += base_amount_dict.getGeneratedAmountQuantity(
self._dict[base_amount] = value *variated_base_amount)
self._dict[variated_base_amount] = value
return value return value
getBaseAmountList__roles__ = None # public getBaseAmountList__roles__ = None # public
...@@ -100,7 +105,7 @@ class BaseAmountDict(Implicit): ...@@ -100,7 +105,7 @@ class BaseAmountDict(Implicit):
return list(self._amount_list) return list(self._amount_list)
getGeneratedAmountQuantity__roles__ = None # public getGeneratedAmountQuantity__roles__ = None # public
def getGeneratedAmountQuantity(self, base_amount): def getGeneratedAmountQuantity(self, base_amount, variation_category_list=()):
"""Get final computed quantity for given 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
...@@ -108,9 +113,10 @@ class BaseAmountDict(Implicit): ...@@ -108,9 +113,10 @@ class BaseAmountDict(Implicit):
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 base_amount in self._frozen: variated_base_amount = base_amount, variation_category_list
return self._getQuantity(base_amount) if variated_base_amount in self._frozen:
self._frozen.add(base_amount) return self._getQuantity(variated_base_amount)
self._frozen.add(variated_base_amount)
try: try:
method = self._cache[base_amount] method = self._cache[base_amount]
except KeyError: except KeyError:
...@@ -121,8 +127,13 @@ class BaseAmountDict(Implicit): ...@@ -121,8 +127,13 @@ class BaseAmountDict(Implicit):
if method is None: if method is None:
method = self._amount_generator_line.getBaseAmountQuantity method = self._amount_generator_line.getBaseAmountQuantity
self._cache[base_amount] = method self._cache[base_amount] = method
value = method(self, base_amount, **self._method_kw) if variation_category_list:
self._dict[base_amount] = value kw = dict(self._method_kw,
variation_category_list=variation_category_list)
else:
kw = self._method_kw
value = method(self, base_amount, **kw)
self._dict[variated_base_amount] = value
return value return value
...@@ -218,6 +229,8 @@ class AmountGeneratorMixin: ...@@ -218,6 +229,8 @@ class AmountGeneratorMixin:
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
base_application_list = self.getBaseApplicationList()
base_contribution_list = self.getBaseContributionList()
for cell in amount_generator_cell_list: for cell in amount_generator_cell_list:
if not cell.test(delivery_amount): if not cell.test(delivery_amount):
if cell is self: if cell is self:
...@@ -228,8 +241,8 @@ class AmountGeneratorMixin: ...@@ -228,8 +241,8 @@ class AmountGeneratorMixin:
property_dict = cell_aggregate[key] property_dict = cell_aggregate[key]
except KeyError: except KeyError:
cell_aggregate[key] = property_dict = { cell_aggregate[key] = property_dict = {
'base_application_set': set(), 'base_application_set': set(base_application_list),
'base_contribution_set': set(), 'base_contribution_set': set(base_contribution_list),
'category_list': [], 'category_list': [],
'causality_value_list': [], 'causality_value_list': [],
'efficiency': self.getEfficiency(), 'efficiency': self.getEfficiency(),
...@@ -250,10 +263,10 @@ class AmountGeneratorMixin: ...@@ -250,10 +263,10 @@ 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()
# For final amounts, base_application and id MUST be defined if cell is not self:
# 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())
# For intermediate calculations, base_contribution_list MUST be defined
property_dict['base_contribution_set'].update( property_dict['base_contribution_set'].update(
cell.getBaseContributionList()) cell.getBaseContributionList())
property_dict['causality_value_list'].append(cell) property_dict['causality_value_list'].append(cell)
...@@ -271,6 +284,12 @@ class AmountGeneratorMixin: ...@@ -271,6 +284,12 @@ class AmountGeneratorMixin:
if causality_value is self and len(cell_aggregate) > 1: if causality_value is self and len(cell_aggregate) > 1:
continue continue
base_application_set = property_dict['base_application_set'] base_application_set = property_dict['base_application_set']
# allow a single base_application to be variated
variation_category_list = tuple(sorted([x for x in base_application_set
if x[:12] != 'base_amount/']))
if variation_category_list:
base_application_set.difference_update(variation_category_list)
assert len(base_application_set) == 1
# 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)
...@@ -286,8 +305,9 @@ class AmountGeneratorMixin: ...@@ -286,8 +305,9 @@ 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(map(base_amount.getGeneratedAmountQuantity, quantity = float(sum(base_amount.getGeneratedAmountQuantity(
base_application_set))) base_application, variation_category_list)
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]
...@@ -324,8 +344,16 @@ class AmountGeneratorMixin: ...@@ -324,8 +344,16 @@ class AmountGeneratorMixin:
quantity /= property_dict.get('efficiency', 1) quantity /= property_dict.get('efficiency', 1)
except ZeroDivisionError: except ZeroDivisionError:
quantity *= float('inf') quantity *= float('inf')
for base_contribution in property_dict['base_contribution_set']: base_contribution_set = property_dict['base_contribution_set']
base_amount.contribute(base_contribution, quantity) # 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)
assert len(base_contribution_set) == 1
for base_contribution in base_contribution_set:
base_amount.contribute(base_contribution, variation_category_list,
quantity)
is_mapped_value = isinstance(self, MappedValue) is_mapped_value = isinstance(self, MappedValue)
......
...@@ -56,8 +56,8 @@ class TestBPMMixin(ERP5TypeTestCase): ...@@ -56,8 +56,8 @@ class TestBPMMixin(ERP5TypeTestCase):
def createCategoriesInCategory(self, category, category_id_list): def createCategoriesInCategory(self, category, category_id_list):
for category_id in category_id_list: for category_id in category_id_list:
if not category.hasObject(category_id): if not category.hasObject(category_id):
category.newContent(portal_type='Category', id = category_id, category.newContent(category_id,
title = category_id) title=category_id.replace('_', ' ').title())
@reindex @reindex
def createCategories(self): def createCategories(self):
...@@ -73,6 +73,10 @@ class TestBPMMixin(ERP5TypeTestCase): ...@@ -73,6 +73,10 @@ class TestBPMMixin(ERP5TypeTestCase):
self.createCategoriesInCategory(category_tool.trade_state, self.createCategoriesInCategory(category_tool.trade_state,
['ordered', 'invoiced', 'delivered', 'taxed', ['ordered', 'invoiced', 'delivered', 'taxed',
'state_a', 'state_b', 'state_c', 'state_d', 'state_e']) 'state_a', 'state_b', 'state_c', 'state_d', 'state_e'])
self.createCategoriesInCategory(category_tool, ('tax_range', 'tax_share'))
self.createCategoriesInCategory(category_tool.tax_range,
('0_200', '200_inf'))
self.createCategoriesInCategory(category_tool.tax_share, 'AB')
@reindex @reindex
def createBusinessProcess(self, **kw): def createBusinessProcess(self, **kw):
......
...@@ -37,7 +37,7 @@ from Products.ERP5.tests.testBPMCore import TestBPMMixin ...@@ -37,7 +37,7 @@ from Products.ERP5.tests.testBPMCore import TestBPMMixin
from Products.ERP5Type.Base import Base from Products.ERP5Type.Base import Base
from Products.ERP5Type.Utils import simple_decorator from Products.ERP5Type.Utils import simple_decorator
from DateTime import DateTime from DateTime import DateTime
from Products.ERP5Type.tests.utils import createZODBPythonScript from Products.ERP5Type.tests.utils import createZODBPythonScript, updateCellList
def save_result_as(name): def save_result_as(name):
...@@ -774,6 +774,66 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -774,6 +774,66 @@ class TestTradeModelLine(TestTradeModelLineMixin):
self.checkAggregatedAmountList(order) self.checkAggregatedAmountList(order)
def test_03_VariatedModelLine(self):
base_amount = self.setBaseAmountQuantityMethod('tax', """\
def getBaseAmountQuantity(delivery_amount, base_application,
variation_category_list=(), **kw):
if variation_category_list:
quantity = delivery_amount.getGeneratedAmountQuantity(base_application)
tax_range, = variation_category_list
if tax_range == 'tax_range/0_200':
return min(quantity, 200)
else:
assert tax_range == 'tax_range/200_inf'
return max(0, quantity - 200)
return context.getBaseAmountQuantity(delivery_amount, base_application, **kw)
return getBaseAmountQuantity""")
business_process = self.createBusinessProcess()
trade_condition = self.createTradeCondition(business_process, (
dict(price=0.3,
base_application=base_amount,
reference='tax1',
int_index=10),
dict(base_application=base_amount,
base_contribution='base_amount/total_tax',
reference='tax2',
int_index=20),
dict(base_application='base_amount/total_tax',
base_contribution='base_amount/total',
reference='tax3',
int_index=30),
))
def createCells(line, matrix, base_application=(), base_contribution=()):
range_list = [set() for x in iter(matrix).next()]
for index in matrix:
for x, y in zip(range_list, index):
x.add(y)
line.setCellRange(*range_list)
for index, price in matrix.iteritems():
line.newCell(mapped_value_property='price', price=price,
base_application_list=[index[i] for i in base_application],
base_contribution_list=[index[i] for i in base_contribution],
*index)
createCells(self['trade_model_line/tax2'], {
('tax_range/0_200', 'tax_share/A'): .1,
('tax_range/0_200', 'tax_share/B'): .2,
('tax_range/200_inf', 'tax_share/A'): .3,
('tax_range/200_inf', 'tax_share/B'): .4,
}, base_application=(0,), base_contribution=(1,))
createCells(self['trade_model_line/tax3'], {
('tax_share/A',): .5,
('tax_share/B',): .6,
}, base_application=(0,))
from Products.ERP5Type.Document import newTempAmount
for x in ((100, 30, 10, 20, 5, 12),
(500, 150, 20, 90, 40, 120, 55, 96)):
amount = newTempAmount(self.portal, '_',
quantity=x[0], price=1,
base_contribution=base_amount)
amount_list = trade_condition.getGeneratedAmountList((amount,))
self.assertEqual(sorted(x[1:]),
sorted(y.getTotalPrice() for y in amount_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