Commit e2075796 authored by Julien Muchembled's avatar Julien Muchembled

Review new amount generator

- Update API and interfaces
- Make it possible to:
  - do non-linear calculation (via global type-based script)
    for every considered base_amount
    (i.e. not only for base_application of input movements).
    To avoid performance issue, the type-based script is changed to return
    functions instead of calculating results directly.
  - get accumulated values from movements while generating amounts for a
    delivery (i.e. for Amount Generator Lines targetting deliveries).
- Produce amounts even if there is no resource, for reporting.
- Drop probably useless create_line property: checking if there is a resource
  should be enough.
- Prepare refactoring of MRP/Trade/Payroll by introducing AmountGeneratorLine
  class and property sheet.
- Stop doing anything if there is no base_application on an AGL to avoid
  conflicts while figuring out if a movement was created manually or not.
- Update some forms.
- Update testTradeModelLine

git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@39028 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 4c416f78
...@@ -270,10 +270,7 @@ ...@@ -270,10 +270,7 @@
<string>my_view_mode_trade_date</string> <string>my_view_mode_trade_date</string>
<string>my_view_mode_base_application_list</string> <string>my_view_mode_base_application_list</string>
<string>my_view_mode_calculation_script_id</string> <string>my_view_mode_calculation_script_id</string>
<string>my_view_mode_create_line</string>
<string>my_view_mode_ratio_price</string> <string>my_view_mode_ratio_price</string>
<string>my_view_mode_fixed_quantity</string>
<string>my_view_mode_target_level</string>
<string>my_core_mode_total_price</string> <string>my_core_mode_total_price</string>
<string>my_view_mode_listbox_read_only_start_date</string> <string>my_view_mode_listbox_read_only_start_date</string>
<string>my_report_mode_aggregation_level</string> <string>my_report_mode_aggregation_level</string>
...@@ -305,6 +302,7 @@ ...@@ -305,6 +302,7 @@
<string>my_view_mode_listbox_movement_relative_url</string> <string>my_view_mode_listbox_movement_relative_url</string>
<string>my_view_mode_movement_listbox_price</string> <string>my_view_mode_movement_listbox_price</string>
<string>my_view_mode_movement_listbox_quantity</string> <string>my_view_mode_movement_listbox_quantity</string>
<string>my_view_mode_target_delivery</string>
</list> </list>
</value> </value>
</item> </item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
<tuple/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>description</string>
<string>title</string>
<string>default</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_view_mode_fixed_quantity</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>default</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>description</string> </key>
<value> <string>Trade Model Line with a Fixed Quantity. If this is set, Ratio is ignored.</string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_view_mode_quantity</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Fixed Quantity</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<tuple>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
<tuple/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python:test(here.getQuantity(None) is None, \'\', here.getQuantity())</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -12,12 +12,14 @@ ...@@ -12,12 +12,14 @@
<item> <item>
<key> <string>delegated_list</string> </key> <key> <string>delegated_list</string> </key>
<value> <value>
<list/> <list>
<string>title</string>
</list>
</value> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>my_create_line</string> </value> <value> <string>my_view_mode_target_delivery</string> </value>
</item> </item>
<item> <item>
<key> <string>message_values</string> </key> <key> <string>message_values</string> </key>
...@@ -74,16 +76,20 @@ ...@@ -74,16 +76,20 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>field_id</string> </key> <key> <string>field_id</string> </key>
<value> <string>my_view_mode_create_line</string> </value> <value> <string>my_checkbox</string> </value>
</item> </item>
<item> <item>
<key> <string>form_id</string> </key> <key> <string>form_id</string> </key>
<value> <string>Base_viewTradeFieldLibrary</string> </value> <value> <string>Base_viewFieldLibrary</string> </value>
</item> </item>
<item> <item>
<key> <string>target</string> </key> <key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value> <value> <string>Click to edit the target</string> </value>
</item> </item>
<item>
<key> <string>title</string> </key>
<value> <string>Target Delivery</string> </value>
</item>
</dictionary> </dictionary>
</value> </value>
</item> </item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
<tuple/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
<string>items</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_view_mode_target_level</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_list_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Target Level</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<tuple>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
<tuple/>
</tuple>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python:((\'\', \'\'), (\'Delivery Level\', modules[\'Products.ERP5.PropertySheet.TradeModelLine\'].TARGET_LEVEL_DELIVERY), (\'Movement Level\', modules[\'Products.ERP5.PropertySheet.TradeModelLine\'].TARGET_LEVEL_MOVEMENT))</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -107,8 +107,8 @@ ...@@ -107,8 +107,8 @@
<string>my_resource_title</string> <string>my_resource_title</string>
<string>my_price</string> <string>my_price</string>
<string>my_quantity</string> <string>my_quantity</string>
<string>my_efficiency</string> <string>my_trade_phase</string>
<string>my_create_line</string> <string>my_use</string>
</list> </list>
</value> </value>
</item> </item>
...@@ -116,10 +116,9 @@ ...@@ -116,10 +116,9 @@
<key> <string>right</string> </key> <key> <string>right</string> </key>
<value> <value>
<list> <list>
<string>my_trade_phase</string> <string>my_target_delivery</string>
<string>my_base_application_list</string> <string>my_base_application_list</string>
<string>my_base_contribution_list</string> <string>my_base_contribution_list</string>
<string>my_use</string>
</list> </list>
</value> </value>
</item> </item>
......
...@@ -74,11 +74,11 @@ ...@@ -74,11 +74,11 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>field_id</string> </key> <key> <string>field_id</string> </key>
<value> <string>my_view_mode_fixed_quantity</string> </value> <value> <string>my_view_mode_amount_generator_quantity</string> </value>
</item> </item>
<item> <item>
<key> <string>form_id</string> </key> <key> <string>form_id</string> </key>
<value> <string>Base_viewTradeFieldLibrary</string> </value> <value> <string>Base_viewFieldLibrary</string> </value>
</item> </item>
<item> <item>
<key> <string>target</string> </key> <key> <string>target</string> </key>
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>my_efficiency</string> </value> <value> <string>my_target_delivery</string> </value>
</item> </item>
<item> <item>
<key> <string>message_values</string> </key> <key> <string>message_values</string> </key>
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>field_id</string> </key> <key> <string>field_id</string> </key>
<value> <string>my_view_mode_efficiency</string> </value> <value> <string>my_view_mode_target_delivery</string> </value>
</item> </item>
<item> <item>
<key> <string>form_id</string> </key> <key> <string>form_id</string> </key>
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import zope.interface
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.XMLMatrix import XMLMatrix
from Products.ERP5.Document.Amount import Amount
from Products.ERP5.Document.MappedValue import MappedValue
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
class AmountGeneratorLine(MappedValue, XMLMatrix, Amount,
AmountGeneratorMixin):
"""Abstract class to represent amount transformation for movements"""
meta_type = 'ERP5 Amount Generator Line'
portal_type = 'Amount Generator Line'
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative interfaces
zope.interface.implements(interfaces.IAmountGeneratorLine)
# Declarative properties
property_sheets = (PropertySheet.AmountGeneratorLine, )
security.declareProtected(Permissions.AccessContentsInformation,
'getCellAggregateKey')
def getCellAggregateKey(self):
"""Define a key in order to aggregate amounts at cell level"""
return (self.getResource(),
self.getVariationText()) # Variation UID, Hash ?
security.declareProtected(Permissions.AccessContentsInformation,
'getBaseAmountQuantity')
@classmethod
def getBaseAmountQuantity(cls, delivery_amount, base_application, rounding):
"""Default method to compute quantity for the given base_application"""
value = delivery_amount.getGeneratedAmountQuantity(base_application)
if base_application in delivery_amount.getBaseContributionList():
value += cls._getBaseAmountQuantity(delivery_amount)
return value
@classmethod
def _getBaseAmountQuantity(cls, delivery_amount):
"""Get default quantity contributed by the input amount"""
raise NotImplementedError
...@@ -28,19 +28,11 @@ ...@@ -28,19 +28,11 @@
# #
############################################################################## ##############################################################################
import zope.interface
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type import Permissions, PropertySheet, interfaces from Products.ERP5.Document.AmountGeneratorLine import AmountGeneratorLine
from Products.ERP5Type.XMLMatrix import XMLMatrix
from Products.ERP5.Document.Amount import Amount
from Products.ERP5.Document.MappedValue import MappedValue
from Products.ERP5.AggregatedAmountList import AggregatedAmountList
from Products.ERP5.Document.TradeCondition import TradeCondition
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
import zope.interface
class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin): class TradeModelLine(AmountGeneratorLine):
"""Trade Model Line is a way to represent trade transformation for movements""" """Trade Model Line is a way to represent trade transformation for movements"""
meta_type = 'ERP5 Trade Model Line' meta_type = 'ERP5 Trade Model Line'
portal_type = 'Trade Model Line' portal_type = 'Trade Model Line'
...@@ -49,34 +41,18 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin): ...@@ -49,34 +41,18 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin):
security = ClassSecurityInfo() security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation) security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative interfaces
zope.interface.implements(
interfaces.IAmountGenerator,
interfaces.IVariated
)
# Declarative properties # Declarative properties
property_sheets = ( PropertySheet.Base property_sheets = (PropertySheet.TradeModelLine, )
, PropertySheet.SimpleItem
, PropertySheet.CategoryCore
, PropertySheet.Amount
, PropertySheet.Price
, PropertySheet.TradeModelLine
, PropertySheet.Reference
, PropertySheet.Predicate
, PropertySheet.MappedValue
)
# XXX to be specificied in an interface (IAmountGeneratorLine ?) @classmethod
def getAmountProperty(self, amount, base_application, amount_list, rounding): def _getBaseAmountQuantity(cls, delivery_amount):
""" return delivery_amount.getTotalPrice()
Produced amount quantity is needed to initialize transformation
"""
return amount.getTotalPrice()
### Mapped Value Definition ### Mapped Value Definition
# Provide default mapped value properties and categories if # Provide default mapped value properties and categories if
# not defined # not defined
security.declareProtected(Permissions.AccessContentsInformation,
'getMappedValuePropertyList')
def getMappedValuePropertyList(self): def getMappedValuePropertyList(self):
""" """
""" """
...@@ -90,10 +66,7 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin): ...@@ -90,10 +66,7 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin):
return ('price', 'efficiency') return ('price', 'efficiency')
def getMappedValueBaseCategoryList(self): def getMappedValueBaseCategoryList(self):
result = self._baseGetMappedValueBaseCategoryList() return self._baseGetMappedValueBaseCategoryList() or ('trade_phase',)
if result:
return result
return ('base_contribution', 'trade_phase', )
# #
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
......
...@@ -104,14 +104,15 @@ class TradeModelRuleMovementGenerator(MovementGeneratorMixin): ...@@ -104,14 +104,15 @@ class TradeModelRuleMovementGenerator(MovementGeneratorMixin):
amount_generator_type_list=('Purchase Trade Condition', amount_generator_type_list=('Purchase Trade Condition',
'Sale Trade Condition', 'Sale Trade Condition',
'Trade Model Line')): 'Trade Model Line')):
# FIXME: Is it the right way to have source/destination and other if amount.getResource():
# non-Amount properties set on the generated movement ? # FIXME: Is it the right way to have source/destination and other
movement = input_movement.asContext(**dict((k, v) # non-Amount properties set on the generated movement ?
for k, v in amount.__dict__.iteritems() movement = input_movement.asContext(**dict((k, v)
if k[0] != '_' and k != 'categories')) for k, v in amount.__dict__.iteritems()
base_category_set = set(amount.getBaseCategoryList()) if k[0] != '_' and k != 'categories'))
base_category_set.remove('price_currency') # XXX base_category_set = set(amount.getBaseCategoryList())
movement._setCategoryMembership(base_category_set, base_category_set.remove('price_currency') # XXX
amount.getCategoryList(), movement._setCategoryMembership(base_category_set,
base=True) amount.getCategoryList(),
yield movement base=True)
yield movement
...@@ -81,17 +81,15 @@ class TransformedResource(MappedValue, XMLMatrix, Amount): ...@@ -81,17 +81,15 @@ class TransformedResource(MappedValue, XMLMatrix, Amount):
# Provide default mapped value properties and categories if # Provide default mapped value properties and categories if
# not defined # not defined
def getMappedValuePropertyList(self): def getMappedValuePropertyList(self):
result = self._baseGetMappedValuePropertyList() return self._baseGetMappedValuePropertyList() or (
if result: 'converted_quantity', 'efficiency')
return result
return ('quantity',)
def getMappedValueBaseCategoryList(self): def getMappedValueBaseCategoryList(self):
result = self._baseGetMappedValueBaseCategoryList() result = self._baseGetMappedValueBaseCategoryList()
if not result: if not result:
if not self.hasCellContent(base_id='variation'): if not self.hasCellContent(base_id='variation'):
result = self.getVariationRangeBaseCategoryList() # The current resource variation result = self.getVariationRangeBaseCategoryList() # The current resource variation
return list(result) + ['quantity_unit'] return result
def getBaseApplication(self): def getBaseApplication(self):
""" """
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
class AmountGeneratorLine:
"""
Properties for amount generator lines
"""
_properties = (
{ 'id' : 'target_delivery',
'description' : "Defines if amount generator line should be applied on"
" delivery (e.g. a stamp on an order) instead of"
" movements (e.g. VAT to every order line).",
'type' : 'boolean',
'mode' : 'w',
},
)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
# Łukasz Nowak <luke@nexedi.com>
# #
# WARNING: This program as such is intended to be used by professional # WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential # programmers who take the whole responsability of assessing all potential
...@@ -26,23 +25,10 @@ ...@@ -26,23 +25,10 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# #
############################################################################## ##############################################################################
from AccessControl import ModuleSecurityInfo
class TradeModelLine: class TradeModelLine:
""" """
Properties for trade model lines Properties for trade model lines
""" """
_properties = ( _categories = ('trade_phase',)
{ 'id' : 'create_line',
'description' : 'A flag indicating if the corresponding line will'
' be created',
'type' : 'boolean',
'mode' : 'w',
'default' : True,
},
)
_categories = (
'base_application', 'base_contribution', 'trade_phase',
)
...@@ -182,6 +182,7 @@ ...@@ -182,6 +182,7 @@
<string>listbox_modification_date</string> <string>listbox_modification_date</string>
<string>listbox_creation_date</string> <string>listbox_creation_date</string>
<string>listbox_owner_title</string> <string>listbox_owner_title</string>
<string>my_view_mode_amount_generator_quantity</string>
</list> </list>
</value> </value>
</item> </item>
......
...@@ -2,10 +2,7 @@ ...@@ -2,10 +2,7 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<tuple> <global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
<tuple/>
</tuple>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
...@@ -13,15 +10,13 @@ ...@@ -13,15 +10,13 @@
<key> <string>delegated_list</string> </key> <key> <string>delegated_list</string> </key>
<value> <value>
<list> <list>
<string>description</string>
<string>title</string>
<string>default</string> <string>default</string>
</list> </list>
</value> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>my_view_mode_create_line</string> </value> <value> <string>my_view_mode_amount_generator_quantity</string> </value>
</item> </item>
<item> <item>
<key> <string>message_values</string> </key> <key> <string>message_values</string> </key>
...@@ -83,12 +78,12 @@ ...@@ -83,12 +78,12 @@
<value> <value>
<dictionary> <dictionary>
<item> <item>
<key> <string>description</string> </key> <key> <string>default</string> </key>
<value> <string>Useful for intermediated lines.</string> </value> <value> <string></string> </value>
</item> </item>
<item> <item>
<key> <string>field_id</string> </key> <key> <string>field_id</string> </key>
<value> <string>my_checkbox</string> </value> <value> <string>my_quantity</string> </value>
</item> </item>
<item> <item>
<key> <string>form_id</string> </key> <key> <string>form_id</string> </key>
...@@ -98,10 +93,6 @@ ...@@ -98,10 +93,6 @@
<key> <string>target</string> </key> <key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value> <value> <string>Click to edit the target</string> </value>
</item> </item>
<item>
<key> <string>title</string> </key>
<value> <string>Create Line</string> </value>
</item>
</dictionary> </dictionary>
</value> </value>
</item> </item>
...@@ -110,16 +101,13 @@ ...@@ -110,16 +101,13 @@
</record> </record>
<record id="2" aka="AAAAAAAAAAI="> <record id="2" aka="AAAAAAAAAAI=">
<pickle> <pickle>
<tuple> <global name="TALESMethod" module="Products.Formulator.TALESField"/>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
<tuple/>
</tuple>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item> <item>
<key> <string>_text</string> </key> <key> <string>_text</string> </key>
<value> <string>here/isCreateLine</string> </value> <value> <string>python: here.getQuantity(None)</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
...@@ -82,3 +82,33 @@ class IAmountGenerator(Interface): ...@@ -82,3 +82,33 @@ class IAmountGenerator(Interface):
- implement rounding appropriately (True or False seems - implement rounding appropriately (True or False seems
simplistic) simplistic)
""" """
class IAmountGeneratorLine(Interface):
"""Amount Generator Line interface specification
"""
def getCellAggregateKey():
"""Define a key in order to aggregate amounts at cell level
Transformed Resource (Transformation)
key must be None because:
- quantity and variation are defined in different cells so that the
user does not need to enter values depending on all axes
- amount_generator_cell.test should filter only 1 variant
current key = (acquired resource, acquired variation)
Assorted Resource (Transformation)
key = (assorted resource, assorted resource variation)
usually resource and quantity provided together
Payroll
key = (payroll resource, payroll resource variation)
Tax
key = (tax resource, tax resource variation)
"""
def getBaseAmountQuantity(delivery_amount, base_application, rounding):
"""Default method to compute quantity for the given base_application
"""
...@@ -28,12 +28,90 @@ ...@@ -28,12 +28,90 @@
import random import random
import zope.interface import zope.interface
from zLOG import LOG, WARNING
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, interfaces from Products.ERP5Type import Permissions, interfaces
from Products.ERP5.Document.Amount import Amount from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5.Document.MappedValue import MappedValue from Products.ERP5.Document.MappedValue import MappedValue
# XXX What should be done when there is no base_application ?
# There are 2 options:
# 1. Make the amount generator line always apply, once, which provides an
# easy way to generator a fixed quantity.
# 2. Use this criterion to know if a movement was created manually.
# This is required to not generate amounts from movements that
# are already the result of generated amounts.
# Old simulation implemented both but they conflict.
# Current code implements the 2nd option: Should we use 'use' instead ?
class BaseAmount(dict):
"""Dictionary holding accumulated base amounts
"""
def __init__(self, context, cache, method_kw):
self._context = context
self._frozen = set()
self._lazy = []
self._cache = cache
self._method_kw = method_kw
def getContext(self):
return self._context
def updateCache(self, base_amount_set, amount_generator_line):
cache = self._cache
base_amount_set = base_amount_set.difference(cache)
if base_amount_set:
method = amount_generator_line._getTypeBasedMethod(
'getBaseAmountQuantityMethod')
for base_amount in base_amount_set:
if method is None:
cache[base_amount] = amount_generator_line.getBaseAmountQuantity
else:
cache[base_amount] = method(base_amount)
def recurse(self, portal_type=None):
for amount in self._context.objectValues(portal_type=portal_type):
# Add only movement which are input. Output will be recalculated.
# XXX See above comment about the absence of base_application
# (for example, we could check if resource use category is in the
# normal resource use preference list).
if not amount.getBaseApplication():
base_amount = self.__class__(amount, self._cache, self._method_kw)
self._lazy.append(base_amount)
for base_amount in base_amount.recurse(portal_type):
yield base_amount
yield self
def __getitem__(self, key):
"""Get intermediate computed quantity for given base_application"""
if key in self._frozen:
raise ValueError("Can not contribute to %r because this base_amount is"
" already applied. Order of Amount Generator Lines is"
" wrong." % key)
try:
return dict.__getitem__(self, key)
except KeyError:
value = 0
for lazy in self._lazy:
value += lazy.getQuantity(key)
self[key] = value
return value
def getQuantity(self, key):
"""Get final computed quantity for given base_application
Note: During a call to getQuantity, this method may be called again by
getGeneratedAmountQuantity for the same amount and key.
In this case, the returned value is the last intermediate value just
before finalization.
"""
if key in self._frozen:
return dict.__getitem__(self, key)
self[key] # initialize entry before we freeze it
self._frozen.add(key)
self[key] = value = self._cache[key](self._context, key, **self._method_kw)
return value
class AmountGeneratorMixin: class AmountGeneratorMixin:
""" """
...@@ -45,8 +123,6 @@ class AmountGeneratorMixin: ...@@ -45,8 +123,6 @@ class AmountGeneratorMixin:
methods 'getMappedValuePropertyList' and 'getMappedValueBaseCategoryList' methods 'getMappedValuePropertyList' and 'getMappedValueBaseCategoryList'
to gather the properties and categories to copy from the model to gather the properties and categories to copy from the model
to the generated amounts. to the generated amounts.
NOTE: this is an first prototype of implementation
""" """
# Declarative security # Declarative security
...@@ -56,29 +132,13 @@ class AmountGeneratorMixin: ...@@ -56,29 +132,13 @@ class AmountGeneratorMixin:
# Declarative interfaces # Declarative interfaces
zope.interface.implements(interfaces.IAmountGenerator,) zope.interface.implements(interfaces.IAmountGenerator,)
# XXX to be specificied in an interface (IAmountGeneratorLine ?) security.declareProtected(Permissions.AccessContentsInformation,
def getCellAggregateKey(self, amount_generator_cell): 'getGeneratedAmountQuantity')
"""Define a key in order to aggregate amounts at cell level def getGeneratedAmountQuantity(self, base_application):
"""Give access to computed quantities during generation of amounts"""
Transformed Resource (Transformation) base_amount = getTransactionalVariable()[
key must be None because: 'amount_generator.getGeneratedAmountList'][self]
- quantity and variation are defined in different cells so that the return base_amount.getQuantity(base_application)
user does not need to enter values depending on all axes
- amount_generator_cell.test should filter only 1 variant
current key = (acquired resource, acquired variation)
Assorted Resource (Transformation)
key = (assorted resource, assorted resource variation)
usually resource and quantity provided together
Payroll
key = (payroll resource, payroll resource variation)
Tax
key = (tax resource, tax resource variation)
"""
return (amount_generator_cell.getResource(),
amount_generator_cell.getVariationText()) # Variation UID, Hash ?
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'getGeneratedAmountList') 'getGeneratedAmountList')
...@@ -90,7 +150,6 @@ class AmountGeneratorMixin: ...@@ -90,7 +150,6 @@ class AmountGeneratorMixin:
list of amounts without any aggregation. list of amounts without any aggregation.
TODO: TODO:
- getTargetLevel support
- is rounding really well supported (ie. before and after aggregation) - is rounding really well supported (ie. before and after aggregation)
very likely not - proxying before or after must be decided very likely not - proxying before or after must be decided
""" """
...@@ -106,60 +165,32 @@ class AmountGeneratorMixin: ...@@ -106,60 +165,32 @@ class AmountGeneratorMixin:
# Set empty result by default # Set empty result by default
result = [] result = []
args = (getTransactionalVariable().setdefault(
"amount_generator.BaseAmount", {}),
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():
# Amounts are sorted to process deeper objects first. base_amount_list = BaseAmount(self, *args) \
movement_portal_type_list = self.getPortalMovementTypeList() .recurse(amount_generator_type_list)
amount_list = [self]
amount_index = 0
while amount_index < len(amount_list):
amount_list += amount_list[amount_index].objectValues(
portal_type=movement_portal_type_list)
amount_index += 1
# Add only movement which are input (i.e. resource use category
# is in the normal resource use preference list). Output will
# be recalculated.
amount_list = [x for x in amount_list[:0:-1] # skip self
if not x.getBaseApplication()] + [self]
elif self.providesIAmount(): elif self.providesIAmount():
amount_list = self, base_amount_list = BaseAmount(self, *args),
elif self.providesIAmountList(): elif self.providesIAmountList():
amount_list = self base_amount_list = (BaseAmount(amount, *args) for amount in self)
else: else:
raise ValueError( raise ValueError("%r must implement IMovementCollection, IAmount or"
'self must implement IMovementCollection, IAmount or IAmountList') " IAmountList" % self)
else:
base_amount_list = (BaseAmount(amount, *args) for amount in amount_list)
def getAmountProperty(amount_generator_line, base_application): # First define the method that will browse recursively
"""Produced amount quantity is needed to initialize transformation"""
if base_application in base_contribution_set:
method = amount_generator_line._getTypeBasedMethod('getAmountProperty')
if method is not None:
value = method(delivery_amount, base_application, amount_list,
rounding)
if value is not None:
return value
return amount_generator_line.getAmountProperty(
delivery_amount, base_application, amount_list, rounding)
# First define the method that will browses recursively
# the amount generator lines and accumulate applicable values # the amount generator lines and accumulate applicable values
def accumulateAmountList(self): def accumulateAmountList(self):
amount_generator_line_list = self.contentValues( amount_generator_line_list = self.contentValues(
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:
# Append lines with missing or duplicate int_index
if self in check_wrong_index_set:
check_wrong_index_set.update(amount_generator_line_list)
else:
index_dict = {}
for line in amount_generator_line_list:
index_dict.setdefault(line.getIntIndex(), []).append(line)
for line_list in index_dict.itervalues():
if len(line_list) > 1:
check_wrong_index_set.update(line_list)
amount_generator_line_list.sort(key=lambda x: (x.getIntIndex(), amount_generator_line_list.sort(key=lambda x: (x.getIntIndex(),
random.random())) random.random()))
for amount_generator_line in amount_generator_line_list: for amount_generator_line in amount_generator_line_list:
...@@ -167,125 +198,91 @@ class AmountGeneratorMixin: ...@@ -167,125 +198,91 @@ class AmountGeneratorMixin:
return return
elif (self.getPortalType() not in amount_generator_line_type_list): elif (self.getPortalType() not in amount_generator_line_type_list):
return return
if not getattr(delivery_amount, self.isTargetDelivery() and
'isDelivery' or 'isMovement')():
return
# 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.contentValues(
portal_type=amount_generator_cell_type_list) portal_type=amount_generator_cell_type_list)
resource_amount_aggregate = {} # aggregates final line information cell_aggregate = {} # aggregates final line information
value_amount_aggregate = {} # aggregates intermediate line information
for amount_generator_cell in amount_generator_cell_list: for cell in amount_generator_cell_list:
if not amount_generator_cell.test(delivery_amount): if not cell.test(delivery_amount):
if cell is self:
return
continue continue
base_application_list = amount_generator_cell.getBaseApplicationList() key = cell.getCellAggregateKey()
try: try:
base_contribution_list = \ application_dict = cell_aggregate[key]
amount_generator_cell.getBaseContributionList() except KeyError:
except AttributeError: cell_aggregate[key] = property_dict = {
base_contribution_list = () 'base_application_set': set(),
resource = amount_generator_cell.getResource() 'base_contribution_set': set(),
if resource or base_contribution_list: # case 1 & 2 'category_list': [],
applied_base_amount_set.update(base_application_list) 'causality_value_list': [],
# XXX What should be done when there is no base_application ? # XXX If they are several cells, we have duplicate references.
# With the following code, it always applies, once, like in 'reference': self.getReference(),
# the old implementation, but this is not consistent with }
# the way we ignore automatically created movements # Then collect the mapped values (quantity, price, trade_phase...)
# (see above code when self provides IMovementCollection). for key in cell.getMappedValuePropertyList():
# We should either do nothing if there is no base_application, # XXX-JPS Make sure handling of list properties can be handled
# or find a criterion other than base_application to find property_dict[key] = cell.getProperty(key)
# manually created movements. category_list = cell.getAcquiredCategoryMembershipList(
for base_application in base_application_list or (None,): cell.getMappedValueBaseCategoryList(), base=1)
if base_application not in base_amount: property_dict['category_list'] += category_list
value = getAmountProperty(self, base_application) property_dict['resource'] = cell.getResource()
if value is None: # For final amounts, base_application and id MUST be defined
continue property_dict['base_application_set'].update(
base_amount[base_application] = value cell.getBaseApplicationList())
# Case 1: the cell defines a final amount of resource # For intermediate calculations, base_contribution_list MUST be defined
if resource: property_dict['base_contribution_set'].update(
key = self.getCellAggregateKey(amount_generator_cell) cell.getBaseContributionList())
property_dict = resource_amount_aggregate.setdefault(key, {}) property_dict['causality_value_list'].append(cell)
# Then collect the mapped properties (net_converted_quantity,
# resource, quantity, base_contribution_list, base_application...) for property_dict in cell_aggregate.itervalues():
for key in amount_generator_cell.getMappedValuePropertyList():
# XXX-JPS Make sure handling of list properties can be handled
property_dict[key] = amount_generator_cell.getProperty(key)
category_list = amount_generator_cell.getAcquiredCategoryMembershipList(
amount_generator_cell.getMappedValueBaseCategoryList(), base=1)
if category_list:
property_dict.setdefault('category_list',
[]).extend(category_list)
property_dict['resource'] = resource
# For final amounts, base_application and id MUST be defined
property_dict.setdefault('base_application_set',
set()).add(base_application)
#property_dict['trade_phase_list'] = amount_generator_cell.getTradePhaseList() # Required moved to MappedValue
property_dict.setdefault('causality_value_list',
[]).append(amount_generator_cell)
# Case 2: the cell defines a temporary calculation line
if base_contribution_list:
# Define a key in order to aggregate amounts in cells
# base_application MUST be defined
#
# Single line case: key = base_application
#
# Payroll
#
# key = base_application
# it is not possible to use cells to add amounts
# in intermediate calculation but only to
# select one amount
#
# key = (base_application, XXX) would be required
#
# Use of a method to generate keys is probably better.
# than hardcoding it here
property_dict = value_amount_aggregate.setdefault(base_application,
{})
# Then collect the mapped properties
for key in amount_generator_cell.getMappedValuePropertyList():
property_dict[key] = amount_generator_cell.getProperty(key)
# For intermediate calculations,
# base_contribution_list MUST be defined
property_dict['base_contribution_list'] = base_contribution_list
for property_dict in resource_amount_aggregate.itervalues():
base_application_set = property_dict['base_application_set'] base_application_set = property_dict['base_application_set']
# property_dict should include # Cache must be prepared with the right context in case that we iterate
# through different kinds of amount generator lines.
base_amount.updateCache(base_application_set, self)
# property_dict may include
# resource - VAT service or a Component in MRP # resource - VAT service or a Component in MRP
# quantity - quantity in component in MRP, (what else XXX) # (if unset, the amount will only be used for reporting)
# variation params - color, size, employer share, etc. # variation params - color, size, employer share, etc.
# one of (net_)(converted_)quantity - used as a multiplier
# -> in MRP, quantity in component
# -> for trade, it provides a way to configure a fixed quantity
# price - empty (like in Transformation) price of a product # price - empty (like in Transformation) price of a product
# (ex. a Stamp) or tax ratio (ie. price per value units) # (ex. a Stamp) or tax ratio (ie. price per value units)
# base_contribution_list - needed to produce reports with # base_contribution_list - needed to produce reports with
# getTotalPrice # getTotalPrice
# # 'efficiency' is stored separately in the generated amount,
# Quantity is used as a multiplier (like in transformations for MRP) # for future simulation of efficiencies (use net_quantity otherwise).
# net_converted_quantity is used preferrably to quantity since we # 'converted_quantity' is used preferrably to 'quantity' since we
# 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 = property_dict.pop('net_converted_quantity', quantity = float(sum(map(base_amount.getQuantity,
property_dict.get('quantity', 1.0)) base_application_set)))
if quantity in (None, ''): for quantity_key in ('net_quantity', 'converted_quantity',
property_dict['quantity'] = sum(base_amount[x] 'net_converted_quantity', 'quantity'):
for x in base_application_set) if quantity_key in property_dict:
else: try:
property_dict['quantity'] = sum(base_amount[x] quantity *= property_dict.pop(quantity_key)
for x in base_application_set) * quantity except ValueError: # None or ''
base_application_set.discard(None) pass
# XXX Is it correct to generate nothing if the computed quantity is 0 ? break
if not property_dict['quantity']: if not quantity:
continue continue
# Create an Amount object # Create an Amount object
# XXX-JPS Could we use a movement for safety ?
amount = newTempAmount(portal, amount = newTempAmount(portal,
# we only want the id to be unique # we only want the id to be unique
property_dict['causality_value_list'][0] property_dict['causality_value_list'][0]
.getRelativeUrl().replace('/', '_')) .getRelativeUrl().replace('/', '_'))
amount._setCategoryList(property_dict.pop('category_list', ())) amount._setCategoryList(property_dict.pop('category_list', ()))
amount._edit( amount._edit(
# XXX If they are several cells, we may have duplicate references. quantity=quantity,
reference=self.getReference(),
# XXX Are title, int_index and description useful ?? # XXX Are title, int_index and description useful ??
title=self.getTitle(), title=self.getTitle(),
int_index=self.getIntIndex(), int_index=self.getIntIndex(),
...@@ -295,52 +292,32 @@ class AmountGeneratorMixin: ...@@ -295,52 +292,32 @@ class AmountGeneratorMixin:
# We hope here that rounding is sufficient at line level # We hope here that rounding is sufficient at line level
amount = getRoundingProxy(amount, context=self) amount = getRoundingProxy(amount, context=self)
result.append(amount) result.append(amount)
for base_application, property_dict in value_amount_aggregate.iteritems(): # Contribute
# property_dict should include base_contribution_set = property_dict['base_contribution_set']
# base_contribution_list - needed to produce reports with if base_contribution_set:
# getTotalPrice quantity *= (property_dict.get('price') or 1) / \
# quantity - quantity in component in MRP, (what else XXX) (property_dict.get('efficiency') or 1)
# price - empty (like in Transformation) price of a product base_amount.updateCache(base_contribution_set, self)
# (ex. a Stamp) or tax ratio (ie. price per value units) for base_contribution in base_contribution_set:
# XXX Why price ? What about efficiency ? base_amount[base_contribution] += quantity
value = base_amount[base_application] * \
(property_dict.get('quantity') or 1.0) * \
(property_dict.get('price') or 1.0) # XXX-JPS is it really 1.0 ?
# Quantity is used as a multiplier
# Price is used as a ratio (also a kind of multiplier)
for base_key in property_dict['base_contribution_list']:
if base_key in applied_base_amount_set:
if self in check_wrong_index_set:
raise ValueError("Duplicate or missing int_index on Amount"
" Generator Lines while processing %r" % self)
else:
LOG("getGeneratedAmountList", WARNING, "%r contributes to %r"
" but this base_amount was already applied. Order of Amount"
" Generator Lines may be wrong." % (self, base_key))
if base_key not in base_amount:
base_amount[base_key] = getAmountProperty(self, base_key) or 0
base_amount[base_key] += value
is_mapped_value = isinstance(self, MappedValue) is_mapped_value = isinstance(self, MappedValue)
# Each amount in amount_list creates a new amount to take into account tv = getTransactionalVariable()
# We thus need to start with a loop on amount_list # backup & restore existing cached value for reentrancy
for delivery_amount in amount_list: 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 = {}
# XXX It should be possible to keep specific keys in base_amount dict. for base_amount in base_amount_list:
# This can be done by a preference listing base_amount categories delivery_amount = base_amount.getContext()
# for which we want to accumulate quantities. base_amount_cache[delivery_amount] = base_amount
base_amount = {None: 1} if not is_mapped_value:
base_contribution_set = delivery_amount.getBaseContributionSet() self = delivery_amount.asComposedDocument(amount_generator_type_list)
# Check that lines are sorted correctly # Browse recursively the amount generator lines and accumulate
applied_base_amount_set = set() # applicable values - now execute the method
# Check that lines with missing or duplicate int_index are independant accumulateAmountList(self)
check_wrong_index_set = set() finally:
# Browse recursively the amount generator lines and accumulate tv['amount_generator.getGeneratedAmountList'] = original_cache
# applicable values - now execute the method
accumulateAmountList(self)
return result return result
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
......
...@@ -33,9 +33,7 @@ import transaction ...@@ -33,9 +33,7 @@ import transaction
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from DateTime import DateTime from DateTime import DateTime
from Products.ERP5Type.tests.utils import createZODBPythonScript, reindex
from Products.CMFCore.utils import getToolByName
from Products.ERP5Type.tests.utils import reindex
class TestBPMMixin(ERP5TypeTestCase): class TestBPMMixin(ERP5TypeTestCase):
"""Skeletons for tests which depend on BPM""" """Skeletons for tests which depend on BPM"""
...@@ -60,9 +58,9 @@ class TestBPMMixin(ERP5TypeTestCase): ...@@ -60,9 +58,9 @@ class TestBPMMixin(ERP5TypeTestCase):
@reindex @reindex
def createCategories(self): def createCategories(self):
category_tool = getToolByName(self.portal, 'portal_categories') category_tool = self.portal.portal_categories
self.createCategoriesInCategory(category_tool.base_amount, ['discount', self.createCategoriesInCategory(category_tool.base_amount, ['discount',
'tax', 'total_tax', 'total_discount', 'total']) 'tax', 'total_tax', 'total_discount', 'total', 'fixed_quantity'])
self.createCategoriesInCategory(category_tool.use, self.createCategoriesInCategory(category_tool.use,
self.normal_resource_use_category_list + \ self.normal_resource_use_category_list + \
self.invoicing_resource_use_category_list) self.invoicing_resource_use_category_list)
......
...@@ -57,6 +57,35 @@ class TestTradeModelLineMixin(TestBPMMixin, UserDict): ...@@ -57,6 +57,35 @@ class TestTradeModelLineMixin(TestBPMMixin, UserDict):
# Constants and variables shared by tests # Constants and variables shared by tests
base_unit_quantity = 0.01 base_unit_quantity = 0.01
def setBaseAmountQuantityMethod(self, base_amount_id, text):
"""Populate TradeModelLine_getBaseAmountQuantityMethod shared script
This helper method edits the script so that:
- there's no need to do any cleanup
- data produced by previous still behaves as expected
"""
base_amount = self.portal.portal_categories.base_amount
try:
base_amount = base_amount[self._testMethodName]
except KeyError:
base_amount = base_amount.newContent(self._testMethodName)
try:
return base_amount[base_amount_id].getRelativeUrl()
except KeyError:
base_amount = base_amount.newContent(base_amount_id).getRelativeUrl()
skin = self.portal.portal_skins.custom
script_id = 'TradeModelLine_getBaseAmountQuantityMethod'
test = "\nif base_application == %r:\n " % base_amount
try:
old_text = '\n' + skin[script_id].body()
except KeyError:
old_text = "\nreturn context.getBaseAmountQuantity"
else:
skin._delObject(script_id)
text = test + '\n '.join(text.splitlines()) + old_text
createZODBPythonScript(skin, script_id, "base_application", text)
return base_amount
def afterSetUp(self): def afterSetUp(self):
UserDict.__init__(self) UserDict.__init__(self)
return TestBPMMixin.afterSetUp(self) return TestBPMMixin.afterSetUp(self)
...@@ -731,13 +760,21 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -731,13 +760,21 @@ class TestTradeModelLine(TestTradeModelLineMixin):
to say "discount 10 euros" or "pay more 10 euros" instead of saying "10% to say "discount 10 euros" or "pay more 10 euros" instead of saying "10%
discount from total" discount from total"
""" """
fixed_quantity = self.setBaseAmountQuantityMethod('fixed_quantity', """\
return lambda *args, **kw: 1""")
tax = self.createServiceTax() tax = self.createServiceTax()
trade_condition = self.createTradeCondition((), ( trade_condition = self.createTradeCondition((), (
# create a model line with 100 euros # create a model line with 100 euros
dict(reference='A', resource_value=tax, quantity=100, price=1), dict(reference='A', resource_value=tax, quantity=100, price=1),
# add a discount of 10 euros # add a discount of 10 euros
dict(reference='B', resource_value=tax, quantity=10, price=-1))) dict(reference='B', resource_value=tax, quantity=10, price=-1)))
order = self.createOrder(trade_condition) order = self.createOrder(trade_condition, (
dict(),
))
self.assertEqual([], order.getAggregatedAmountList())
for line in trade_condition.objectValues():
line.setBaseApplication(fixed_quantity)
amount_list = order.getAggregatedAmountList() amount_list = order.getAggregatedAmountList()
self.assertEqual([-10, 100], sorted(x.getTotalPrice() for x in amount_list)) self.assertEqual([-10, 100], sorted(x.getTotalPrice() for x in amount_list))
...@@ -904,31 +941,27 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -904,31 +941,27 @@ class TestTradeModelLine(TestTradeModelLineMixin):
and trade model line can works with appropriate context(delivery or and trade model line can works with appropriate context(delivery or
movement) only. movement) only.
""" """
bounded_fee = self.setBaseAmountQuantityMethod('bounded_fee', """\
return lambda *args, **kw: min(800,
context.getBaseAmountQuantity(*args, **kw))""")
fixed_quantity = self.setBaseAmountQuantityMethod('fixed_quantity', """\
return lambda *args, **kw: 1""")
tax = self.createServiceTax() tax = self.createServiceTax()
trade_condition = self.createTradeCondition(self.createBusinessProcess()) trade_condition = self.createTradeCondition(self.createBusinessProcess())
# create a model line and set target level to `delivery`. # create a model line and set target level to `delivery`.
# XXX When it is possible to accumulate contributed quantities between
# input amounts, the trade condition should be configured as follows:
# tml1: - price=1, no resource
# - base_application='base_amount/tax'
# - base_contribution='base_amount/some_accumulating_category'
# tml2: - price=0.05, resource=tax
# - base_application='base_amount/some_accumulating_category'
# - test_method_id='isDelivery'
# And remove 'base_amount/tax' from base_contribution_list on order.
tml = self.createTradeModelLine(trade_condition, tml = self.createTradeModelLine(trade_condition,
reference='TAX', reference='TAX',
resource_value=tax, resource_value=tax,
base_application='base_amount/tax', base_application='base_amount/tax',
test_method_id='isDelivery', target_delivery=True,
price=0.05) price=0.05)
# create an order. # create an order.
resource_A = self.createResource('Product', title='A') resource_A = self.createResource('Product', title='A')
resource_B = self.createResource('Product', title='B') resource_B = self.createResource('Product', title='B')
order = self.createOrder(trade_condition) order = self.createOrder(trade_condition)
base_contribution_list = 'base_amount/tax', 'base_amount/extra_fee' base_contribution_list = 'base_amount/tax', bounded_fee
order.setBaseContributionList(base_contribution_list)
kw = {'portal_type': self.order_line_portal_type, kw = {'portal_type': self.order_line_portal_type,
'base_contribution_list': base_contribution_list} 'base_contribution_list': base_contribution_list}
order_line_1 = order.newContent(price=1000, quantity=1, order_line_1 = order.newContent(price=1000, quantity=1,
...@@ -939,7 +972,7 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -939,7 +972,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
self.assertEqual([75], [x.getTotalPrice() for x in amount_list]) self.assertEqual([75], [x.getTotalPrice() for x in amount_list])
# change target level to `movement`. # change target level to `movement`.
tml.setTestMethodId('isMovement') tml.setTargetDelivery(False)
amount_list = order.getGeneratedAmountList() amount_list = order.getGeneratedAmountList()
self.assertEqual([25, 50], sorted(x.getTotalPrice() for x in amount_list)) self.assertEqual([25, 50], sorted(x.getTotalPrice() for x in amount_list))
...@@ -948,32 +981,21 @@ class TestTradeModelLine(TestTradeModelLineMixin): ...@@ -948,32 +981,21 @@ class TestTradeModelLine(TestTradeModelLineMixin):
extra_fee_a = self.createTradeModelLine(trade_condition, extra_fee_a = self.createTradeModelLine(trade_condition,
reference='EXTRA_FEE_A', reference='EXTRA_FEE_A',
resource_value=tax, resource_value=tax,
base_application='base_amount/extra_fee', base_application=bounded_fee,
test_method_id='isMovement',
price=.2) price=.2)
# Use custom script to return a movement which has a fixed value of quantity.
# If a fixed quantity value is set to trade model line directly then it is
# applied to all the movements without matching base_application category.
createZODBPythonScript(
self.portal.portal_skins.custom,
'TradeModelLine_getAmountProperty',
'amount, base_application, *args, **kw',
"""\
if base_application == 'base_amount/extra_fee':
return min(800, amount.getTotalPrice())
""")
# Extra fee b has a fixed quantity so that this trade model line is applied # Extra fee b has a fixed quantity so that this trade model line is applied
# to all movements by force. # to all movements by force.
extra_fee_b = self.createTradeModelLine(trade_condition, extra_fee_b = self.createTradeModelLine(trade_condition,
reference='EXTRA_FEE_B', reference='EXTRA_FEE_B',
resource_value=tax, resource_value=tax,
test_method_id='isMovement', base_application=fixed_quantity,
price=1) price=1)
# for delivery level # for delivery level
discount = self.createTradeModelLine(trade_condition, discount = self.createTradeModelLine(trade_condition,
reference='DISCOUNT_B', reference='DISCOUNT_B',
resource_value=tax, resource_value=tax,
test_method_id='isDelivery', base_application=fixed_quantity,
target_delivery=True,
quantity=10, price=-1) quantity=10, price=-1)
transaction.commit() # flush transactional cache transaction.commit() # flush transactional cache
...@@ -986,16 +1008,14 @@ if base_application == 'base_amount/extra_fee': ...@@ -986,16 +1008,14 @@ if base_application == 'base_amount/extra_fee':
expected_tax = 1000*0.05 + 500*0.05, 500*0.2 + 800*0.2, 1 + 1, -10 expected_tax = 1000*0.05 + 500*0.05, 500*0.2 + 800*0.2, 1 + 1, -10
self.assertEqual(sorted(expected_tax), self.assertEqual(sorted(expected_tax),
sorted(x.getTotalPrice() for x in amount_list)) sorted(x.getTotalPrice() for x in amount_list))
# Change target level # Change target level
extra_fee_a.setTestMethodId('isDelivery') extra_fee_a.setTargetDelivery(True)
extra_fee_b.setTestMethodId('isDelivery') extra_fee_b.setTargetDelivery(True)
amount_list = order.getAggregatedAmountList() amount_list = order.getAggregatedAmountList()
expected_tax = 1000*0.05 + 500*0.05, 800*0.2, 1, -10 expected_tax = 1000*0.05 + 500*0.05, 800*0.2, 1, -10
self.assertEqual(sorted(expected_tax), self.assertEqual(sorted(expected_tax),
sorted(x.getTotalPrice() for x in amount_list)) sorted(x.getTotalPrice() for x in amount_list))
@expectedFailure
def test_tradeModelLineWithRounding(self): def test_tradeModelLineWithRounding(self):
""" """
Test if trade model line works with rounding. Test if trade model line works with rounding.
...@@ -1004,9 +1024,10 @@ if base_application == 'base_amount/extra_fee': ...@@ -1004,9 +1024,10 @@ if base_application == 'base_amount/extra_fee':
# create a model line and set target level to `delivery` # create a model line and set target level to `delivery`
tax = self.createTradeModelLine(trade_condition, tax = self.createTradeModelLine(trade_condition,
reference='TAX', reference='TAX',
base_application_list=['base_amount/tax'], base_application='base_amount/tax',
base_contribution_list=['base_amount/total_tax']) base_contribution='base_amount/total_tax',
tax.edit(price=0.05, target_level=TARGET_LEVEL_DELIVERY) price=0.05,
target_delivery=True)
# create a rounding model for tax # create a rounding model for tax
rounding_model = self.portal.portal_roundings.newContent(portal_type='Rounding Model') rounding_model = self.portal.portal_roundings.newContent(portal_type='Rounding Model')
...@@ -1024,62 +1045,51 @@ if base_application == 'base_amount/extra_fee': ...@@ -1024,62 +1045,51 @@ if base_application == 'base_amount/extra_fee':
order_line_1 = order.newContent(portal_type=self.order_line_portal_type, order_line_1 = order.newContent(portal_type=self.order_line_portal_type,
price=3333, quantity=1, price=3333, quantity=1,
resource_value=resource_A, resource_value=resource_A,
base_contribution_list=['base_amount/tax']) base_contribution='base_amount/tax')
order_line_2 = order.newContent(portal_type=self.order_line_portal_type, order_line_2 = order.newContent(portal_type=self.order_line_portal_type,
price=171, quantity=1, price=171, quantity=1,
resource_value=resource_B, resource_value=resource_B,
base_contribution_list=['base_amount/tax']) base_contribution='base_amount/tax')
transaction.commit() transaction.commit()
self.tic() self.tic()
# check the result without rounding # check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False) amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list)) self.assertEqual((3333+171)*0.05, amount.getTotalPrice()) # 175.2
self.assertEqual(set([order_line_1, order_line_2]),
set(amount_list[0].getCausalityValueList()))
self.assertEqual((3333+171)*0.05, amount_list[0].getTotalPrice())
# check the result with rounding # check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True) amount, = order.getAggregatedAmountList(rounding=True)
self.assertEqual(1, len(amount_list)) self.assertEqual(175, amount.getTotalPrice())
self.assertEqual(set([order_line_1, order_line_2]),
set(amount_list[0].getCausalityValueList()))
self.assertEqual(175, amount_list[0].getTotalPrice())
# change tax trade model line to `movement` level # change tax trade model line to `movement` level
tax.edit(target_level=TARGET_LEVEL_MOVEMENT) tax.setTargetDelivery(False)
def getTotalAmount(amount_list): def getTotalAmount(amount_list):
result = 0 result = 0
for amount in amount_list: for amount in amount_list:
if amount.getBaseContribution() in ('base_amount/total', 'base_amount/total_tax'): if amount.getBaseContribution() in ('base_amount/total',
'base_amount/total_tax'):
result += amount.getTotalPrice() result += amount.getTotalPrice()
return result return result
# check the result without rounding # check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False) amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list)) self.assertEqual(3333*0.05+171*0.05, amount.getTotalPrice()) # 175.2
self.assertEqual(3333*0.05+171*0.05, getTotalAmount(amount_list))
# check the result with rounding # check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True) expectedFailure(order.getAggregatedAmountList)(rounding=True)
self.assertEqual(2, len(amount_list)) self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(174, getTotalAmount(amount_list)) self.assertEqual(174, getTotalAmount(amount_list))
# check getAggregatedAmountList result of each movement # check getAggregatedAmountList result of each movement
# order line 1 # order line 1
amount_list = order_line_1.getAggregatedAmountList(rounding=False) amount, = order_line_1.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list)) self.assertEqual(3333*0.05, amount.getTotalPrice()) # 166.65
self.assertEqual(3333*0.05, amount_list[0].getTotalPrice()) amount, = order_line_1.getAggregatedAmountList(rounding=True)
amount_list = order_line_1.getAggregatedAmountList(rounding=True) self.assertEqual(166, amount.getTotalPrice())
self.assertEqual(1, len(amount_list))
self.assertEqual(166, amount_list[0].getTotalPrice())
# order line 2 # order line 2
amount_list = order_line_2.getAggregatedAmountList(rounding=False) amount, = order_line_2.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list)) self.assertEqual(171*0.05, amount.getTotalPrice()) # 8.55
self.assertEqual(171*0.05, amount_list[0].getTotalPrice()) amount, = order_line_2.getAggregatedAmountList(rounding=True)
amount_list = order_line_2.getAggregatedAmountList(rounding=True) self.assertEqual(8, amount.getTotalPrice())
self.assertEqual(1, len(amount_list))
self.assertEqual(8, amount_list[0].getTotalPrice())
# change rounding model definition # change rounding model definition
rounding_model.setDecimalRoundingOption('ROUND_UP') rounding_model.setDecimalRoundingOption('ROUND_UP')
...@@ -1093,15 +1103,14 @@ if base_application == 'base_amount/extra_fee': ...@@ -1093,15 +1103,14 @@ if base_application == 'base_amount/extra_fee':
self.tic() self.tic()
# check the result without rounding # check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False) amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list)) self.assertEqual(3.3333*3333*0.05+171*0.05, amount.getTotalPrice())
self.assertEqual(3.3333*3333*0.05+171*0.05, getTotalAmount(amount_list))
# check the result with rounding # check the result with rounding
# both quantity and total price will be rounded so that the expression # both quantity and total price will be rounded so that the expression
# should be "round_up(round_up(3.3333 * 3333) * 0.05) + round_up(round_up # should be "round_up(round_up(3.3333 * 3333) * 0.05) + round_up(round_up
# (1* 171) * 0.05)" # (1* 171) * 0.05)"
amount_list = order.getAggregatedAmountList(rounding=True) amount_list = order.getAggregatedAmountList(rounding=True)
self.assertEqual(2, len(amount_list)) self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(565, getTotalAmount(amount_list)) self.assertEqual(565, getTotalAmount(amount_list))
# create a rounding model to round quantity property of order line # create a rounding model to round quantity property of order line
...@@ -1119,7 +1128,7 @@ if base_application == 'base_amount/extra_fee': ...@@ -1119,7 +1128,7 @@ if base_application == 'base_amount/extra_fee':
amount_list = order.getAggregatedAmountList(rounding=True) amount_list = order.getAggregatedAmountList(rounding=True)
# The expression should be "round_up(round_up(round_down(3.3333) * 3333) # The expression should be "round_up(round_up(round_down(3.3333) * 3333)
# * 0.05) + round_up(round_up(round_down(1) * 171) * 0.05)" # * 0.05) + round_up(round_up(round_down(1) * 171) * 0.05)"
self.assertEqual(2, len(amount_list)) self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(509, getTotalAmount(amount_list)) self.assertEqual(509, getTotalAmount(amount_list))
# create a rounding model to round price property of order line # create a rounding model to round price property of order line
...@@ -1140,14 +1149,13 @@ if base_application == 'base_amount/extra_fee': ...@@ -1140,14 +1149,13 @@ if base_application == 'base_amount/extra_fee':
self.tic() self.tic()
# check the result without rounding # check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False) amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list)) self.assertEqual(3.3333*3333*0.05+171.1234*0.05, amount.getTotalPrice())
self.assertEqual(3.3333*3333*0.05+171.1234*0.05, getTotalAmount(amount_list))
# check the result with rounding # check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True) amount_list = order.getAggregatedAmountList(rounding=True)
# The expression should be "round_down(3.3333) * round_up(3333) * 0.05 + # The expression should be "round_down(3.3333) * round_up(3333) * 0.05 +
# round_down(1) * round_up(171.1234) * 0.05" # round_down(1) * round_up(171.1234) * 0.05"
self.assertEqual(2, len(amount_list)) self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(508.51000000000005, getTotalAmount(amount_list)) self.assertEqual(508.51000000000005, getTotalAmount(amount_list))
def test_tradeModelLineWithEmptyBaseContributionMovement(self): def test_tradeModelLineWithEmptyBaseContributionMovement(self):
......
...@@ -659,23 +659,31 @@ def registerBaseCategories(property_sheet): ...@@ -659,23 +659,31 @@ def registerBaseCategories(property_sheet):
base_category_dict[bc] = 1 base_category_dict[bc] = 1
def importLocalInterface(module_id, path = None, is_erp5_type=False): def importLocalInterface(module_id, path = None, is_erp5_type=False):
if path is None: def provides(class_id):
instance_home = getConfiguration().instancehome # Create interface getter
path = os.path.join(instance_home, "interfaces") accessor_name = 'provides' + class_id
path = os.path.join(path, "%s.py" % module_id) setattr(BaseClass, accessor_name, lambda self: self.provides(class_id))
f = open(path) BaseClass.security.declarePublic(accessor_name)
try: class_id = "I" + convertToUpperCase(module_id)
class_id = "I" + convertToUpperCase(module_id) if is_erp5_type:
if not is_erp5_type: provides(class_id)
else:
if path is None:
instance_home = getConfiguration().instancehome
path = os.path.join(instance_home, "interfaces")
path = os.path.join(path, "%s.py" % module_id)
f = open(path)
try:
module = imp.load_source(class_id, path, f) module = imp.load_source(class_id, path, f)
import Products.ERP5Type.interfaces finally:
setattr(Products.ERP5Type.interfaces, class_id, getattr(module, class_id)) f.close()
finally: from zope.interface import Interface
f.close() from Products.ERP5Type import interfaces
# Create interface getter InterfaceClass = type(Interface)
accessor_name = 'provides' + class_id for k, v in module.__dict__.iteritems():
setattr(BaseClass, accessor_name, lambda self: self.provides(class_id)) if type(v) is InterfaceClass and v is not Interface:
BaseClass.security.declarePublic(accessor_name) setattr(interfaces, k, v)
provides(class_id)
def importLocalConstraint(class_id, path = None): def importLocalConstraint(class_id, path = None):
import Products.ERP5Type.Constraint import Products.ERP5Type.Constraint
......
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