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 @@
<string>my_view_mode_trade_date</string>
<string>my_view_mode_base_application_list</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_fixed_quantity</string>
<string>my_view_mode_target_level</string>
<string>my_core_mode_total_price</string>
<string>my_view_mode_listbox_read_only_start_date</string>
<string>my_report_mode_aggregation_level</string>
......@@ -305,6 +302,7 @@
<string>my_view_mode_listbox_movement_relative_url</string>
<string>my_view_mode_movement_listbox_price</string>
<string>my_view_mode_movement_listbox_quantity</string>
<string>my_view_mode_target_delivery</string>
</list>
</value>
</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 @@
<item>
<key> <string>delegated_list</string> </key>
<value>
<list/>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_create_line</string> </value>
<value> <string>my_view_mode_target_delivery</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
......@@ -74,16 +76,20 @@
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_view_mode_create_line</string> </value>
<value> <string>my_checkbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewTradeFieldLibrary</string> </value>
<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 Delivery</string> </value>
</item>
</dictionary>
</value>
</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 @@
<string>my_resource_title</string>
<string>my_price</string>
<string>my_quantity</string>
<string>my_efficiency</string>
<string>my_create_line</string>
<string>my_trade_phase</string>
<string>my_use</string>
</list>
</value>
</item>
......@@ -116,10 +116,9 @@
<key> <string>right</string> </key>
<value>
<list>
<string>my_trade_phase</string>
<string>my_target_delivery</string>
<string>my_base_application_list</string>
<string>my_base_contribution_list</string>
<string>my_use</string>
</list>
</value>
</item>
......
......@@ -74,11 +74,11 @@
<dictionary>
<item>
<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>
<key> <string>form_id</string> </key>
<value> <string>Base_viewTradeFieldLibrary</string> </value>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
......
......@@ -17,7 +17,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_efficiency</string> </value>
<value> <string>my_target_delivery</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
......@@ -74,7 +74,7 @@
<dictionary>
<item>
<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>
<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 @@
#
##############################################################################
import zope.interface
from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName
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.AggregatedAmountList import AggregatedAmountList
from Products.ERP5.Document.TradeCondition import TradeCondition
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
import zope.interface
from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5.Document.AmountGeneratorLine import AmountGeneratorLine
class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin):
class TradeModelLine(AmountGeneratorLine):
"""Trade Model Line is a way to represent trade transformation for movements"""
meta_type = 'ERP5 Trade Model Line'
portal_type = 'Trade Model Line'
......@@ -49,34 +41,18 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin):
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative interfaces
zope.interface.implements(
interfaces.IAmountGenerator,
interfaces.IVariated
)
# Declarative properties
property_sheets = ( PropertySheet.Base
, PropertySheet.SimpleItem
, PropertySheet.CategoryCore
, PropertySheet.Amount
, PropertySheet.Price
, PropertySheet.TradeModelLine
, PropertySheet.Reference
, PropertySheet.Predicate
, PropertySheet.MappedValue
)
property_sheets = (PropertySheet.TradeModelLine, )
# XXX to be specificied in an interface (IAmountGeneratorLine ?)
def getAmountProperty(self, amount, base_application, amount_list, rounding):
"""
Produced amount quantity is needed to initialize transformation
"""
return amount.getTotalPrice()
@classmethod
def _getBaseAmountQuantity(cls, delivery_amount):
return delivery_amount.getTotalPrice()
### Mapped Value Definition
# Provide default mapped value properties and categories if
# not defined
security.declareProtected(Permissions.AccessContentsInformation,
'getMappedValuePropertyList')
def getMappedValuePropertyList(self):
"""
"""
......@@ -90,10 +66,7 @@ class TradeModelLine(MappedValue, XMLMatrix, Amount, AmountGeneratorMixin):
return ('price', 'efficiency')
def getMappedValueBaseCategoryList(self):
result = self._baseGetMappedValueBaseCategoryList()
if result:
return result
return ('base_contribution', 'trade_phase', )
return self._baseGetMappedValueBaseCategoryList() or ('trade_phase',)
#
security.declareProtected(Permissions.AccessContentsInformation,
......
......@@ -104,14 +104,15 @@ class TradeModelRuleMovementGenerator(MovementGeneratorMixin):
amount_generator_type_list=('Purchase Trade Condition',
'Sale Trade Condition',
'Trade Model Line')):
# FIXME: Is it the right way to have source/destination and other
# non-Amount properties set on the generated movement ?
movement = input_movement.asContext(**dict((k, v)
for k, v in amount.__dict__.iteritems()
if k[0] != '_' and k != 'categories'))
base_category_set = set(amount.getBaseCategoryList())
base_category_set.remove('price_currency') # XXX
movement._setCategoryMembership(base_category_set,
amount.getCategoryList(),
base=True)
yield movement
if amount.getResource():
# FIXME: Is it the right way to have source/destination and other
# non-Amount properties set on the generated movement ?
movement = input_movement.asContext(**dict((k, v)
for k, v in amount.__dict__.iteritems()
if k[0] != '_' and k != 'categories'))
base_category_set = set(amount.getBaseCategoryList())
base_category_set.remove('price_currency') # XXX
movement._setCategoryMembership(base_category_set,
amount.getCategoryList(),
base=True)
yield movement
......@@ -81,17 +81,15 @@ class TransformedResource(MappedValue, XMLMatrix, Amount):
# Provide default mapped value properties and categories if
# not defined
def getMappedValuePropertyList(self):
result = self._baseGetMappedValuePropertyList()
if result:
return result
return ('quantity',)
return self._baseGetMappedValuePropertyList() or (
'converted_quantity', 'efficiency')
def getMappedValueBaseCategoryList(self):
result = self._baseGetMappedValueBaseCategoryList()
if not result:
if not self.hasCellContent(base_id='variation'):
result = self.getVariationRangeBaseCategoryList() # The current resource variation
return list(result) + ['quantity_unit']
return result
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 -*-
##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
# Łukasz Nowak <luke@nexedi.com>
# 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
......@@ -26,23 +25,10 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from AccessControl import ModuleSecurityInfo
class TradeModelLine:
"""
Properties for trade model lines
"""
_properties = (
{ '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',
)
_categories = ('trade_phase',)
......@@ -182,6 +182,7 @@
<string>listbox_modification_date</string>
<string>listbox_creation_date</string>
<string>listbox_owner_title</string>
<string>my_view_mode_amount_generator_quantity</string>
</list>
</value>
</item>
......
......@@ -2,10 +2,7 @@
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<tuple>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
<tuple/>
</tuple>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
......@@ -13,15 +10,13 @@
<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_create_line</string> </value>
<value> <string>my_view_mode_amount_generator_quantity</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
......@@ -83,12 +78,12 @@
<value>
<dictionary>
<item>
<key> <string>description</string> </key>
<value> <string>Useful for intermediated lines.</string> </value>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_checkbox</string> </value>
<value> <string>my_quantity</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
......@@ -98,10 +93,6 @@
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Create Line</string> </value>
</item>
</dictionary>
</value>
</item>
......@@ -110,16 +101,13 @@
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<tuple>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
<tuple/>
</tuple>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/isCreateLine</string> </value>
<value> <string>python: here.getQuantity(None)</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -82,3 +82,33 @@ class IAmountGenerator(Interface):
- implement rounding appropriately (True or False seems
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 @@
import random
import zope.interface
from zLOG import LOG, WARNING
from AccessControl import ClassSecurityInfo
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
# 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:
"""
......@@ -45,8 +123,6 @@ class AmountGeneratorMixin:
methods 'getMappedValuePropertyList' and 'getMappedValueBaseCategoryList'
to gather the properties and categories to copy from the model
to the generated amounts.
NOTE: this is an first prototype of implementation
"""
# Declarative security
......@@ -56,29 +132,13 @@ class AmountGeneratorMixin:
# Declarative interfaces
zope.interface.implements(interfaces.IAmountGenerator,)
# XXX to be specificied in an interface (IAmountGeneratorLine ?)
def getCellAggregateKey(self, amount_generator_cell):
"""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)
"""
return (amount_generator_cell.getResource(),
amount_generator_cell.getVariationText()) # Variation UID, Hash ?
security.declareProtected(Permissions.AccessContentsInformation,
'getGeneratedAmountQuantity')
def getGeneratedAmountQuantity(self, base_application):
"""Give access to computed quantities during generation of amounts"""
base_amount = getTransactionalVariable()[
'amount_generator.getGeneratedAmountList'][self]
return base_amount.getQuantity(base_application)
security.declareProtected(Permissions.AccessContentsInformation,
'getGeneratedAmountList')
......@@ -90,7 +150,6 @@ class AmountGeneratorMixin:
list of amounts without any aggregation.
TODO:
- getTargetLevel support
- is rounding really well supported (ie. before and after aggregation)
very likely not - proxying before or after must be decided
"""
......@@ -106,60 +165,32 @@ class AmountGeneratorMixin:
# Set empty result by default
result = []
args = (getTransactionalVariable().setdefault(
"amount_generator.BaseAmount", {}),
dict(rounding=rounding))
# If amount_list is None, then try to collect amount_list from
# the current context
if amount_list is None:
if self.providesIMovementCollection():
# Amounts are sorted to process deeper objects first.
movement_portal_type_list = self.getPortalMovementTypeList()
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]
base_amount_list = BaseAmount(self, *args) \
.recurse(amount_generator_type_list)
elif self.providesIAmount():
amount_list = self,
base_amount_list = BaseAmount(self, *args),
elif self.providesIAmountList():
amount_list = self
base_amount_list = (BaseAmount(amount, *args) for amount in self)
else:
raise ValueError(
'self must implement IMovementCollection, IAmount or IAmountList')
raise ValueError("%r must implement IMovementCollection, IAmount or"
" IAmountList" % self)
else:
base_amount_list = (BaseAmount(amount, *args) for amount in amount_list)
def getAmountProperty(amount_generator_line, base_application):
"""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
# First define the method that will browse recursively
# the amount generator lines and accumulate applicable values
def accumulateAmountList(self):
amount_generator_line_list = self.contentValues(
portal_type=amount_generator_line_type_list)
# Recursively feed base_amount
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(),
random.random()))
for amount_generator_line in amount_generator_line_list:
......@@ -167,125 +198,91 @@ class AmountGeneratorMixin:
return
elif (self.getPortalType() not in amount_generator_line_type_list):
return
if not getattr(delivery_amount, self.isTargetDelivery() and
'isDelivery' or 'isMovement')():
return
# Try to collect cells and aggregate their mapped properties
# using resource + variation as aggregation key or base_application
# for intermediate lines
amount_generator_cell_list = [self] + self.contentValues(
portal_type=amount_generator_cell_type_list)
resource_amount_aggregate = {} # aggregates final line information
value_amount_aggregate = {} # aggregates intermediate line information
cell_aggregate = {} # aggregates final line information
for amount_generator_cell in amount_generator_cell_list:
if not amount_generator_cell.test(delivery_amount):
for cell in amount_generator_cell_list:
if not cell.test(delivery_amount):
if cell is self:
return
continue
base_application_list = amount_generator_cell.getBaseApplicationList()
key = cell.getCellAggregateKey()
try:
base_contribution_list = \
amount_generator_cell.getBaseContributionList()
except AttributeError:
base_contribution_list = ()
resource = amount_generator_cell.getResource()
if resource or base_contribution_list: # case 1 & 2
applied_base_amount_set.update(base_application_list)
# XXX What should be done when there is no base_application ?
# With the following code, it always applies, once, like in
# the old implementation, but this is not consistent with
# the way we ignore automatically created movements
# (see above code when self provides IMovementCollection).
# We should either do nothing if there is no base_application,
# or find a criterion other than base_application to find
# manually created movements.
for base_application in base_application_list or (None,):
if base_application not in base_amount:
value = getAmountProperty(self, base_application)
if value is None:
continue
base_amount[base_application] = value
# Case 1: the cell defines a final amount of resource
if resource:
key = self.getCellAggregateKey(amount_generator_cell)
property_dict = resource_amount_aggregate.setdefault(key, {})
# Then collect the mapped properties (net_converted_quantity,
# resource, quantity, base_contribution_list, base_application...)
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():
application_dict = cell_aggregate[key]
except KeyError:
cell_aggregate[key] = property_dict = {
'base_application_set': set(),
'base_contribution_set': set(),
'category_list': [],
'causality_value_list': [],
# XXX If they are several cells, we have duplicate references.
'reference': self.getReference(),
}
# Then collect the mapped values (quantity, price, trade_phase...)
for key in cell.getMappedValuePropertyList():
# XXX-JPS Make sure handling of list properties can be handled
property_dict[key] = cell.getProperty(key)
category_list = cell.getAcquiredCategoryMembershipList(
cell.getMappedValueBaseCategoryList(), base=1)
property_dict['category_list'] += category_list
property_dict['resource'] = cell.getResource()
# For final amounts, base_application and id MUST be defined
property_dict['base_application_set'].update(
cell.getBaseApplicationList())
# For intermediate calculations, base_contribution_list MUST be defined
property_dict['base_contribution_set'].update(
cell.getBaseContributionList())
property_dict['causality_value_list'].append(cell)
for property_dict in cell_aggregate.itervalues():
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
# 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.
# 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
# (ex. a Stamp) or tax ratio (ie. price per value units)
# base_contribution_list - needed to produce reports with
# getTotalPrice
#
# Quantity is used as a multiplier (like in transformations for MRP)
# net_converted_quantity is used preferrably to quantity since we
# need values converted to the default management unit
# 'efficiency' is stored separately in the generated amount,
# for future simulation of efficiencies (use net_quantity otherwise).
# 'converted_quantity' is used preferrably to 'quantity' since we
# need values converted to the default management unit.
# If no quantity is provided, we consider that the value is 1.0
# (XXX is it OK ?) XXX-JPS Need careful review with taxes
quantity = property_dict.pop('net_converted_quantity',
property_dict.get('quantity', 1.0))
if quantity in (None, ''):
property_dict['quantity'] = sum(base_amount[x]
for x in base_application_set)
else:
property_dict['quantity'] = sum(base_amount[x]
for x in base_application_set) * quantity
base_application_set.discard(None)
# XXX Is it correct to generate nothing if the computed quantity is 0 ?
if not property_dict['quantity']:
quantity = float(sum(map(base_amount.getQuantity,
base_application_set)))
for quantity_key in ('net_quantity', 'converted_quantity',
'net_converted_quantity', 'quantity'):
if quantity_key in property_dict:
try:
quantity *= property_dict.pop(quantity_key)
except ValueError: # None or ''
pass
break
if not quantity:
continue
# Create an Amount object
# XXX-JPS Could we use a movement for safety ?
amount = newTempAmount(portal,
# we only want the id to be unique
property_dict['causality_value_list'][0]
.getRelativeUrl().replace('/', '_'))
amount._setCategoryList(property_dict.pop('category_list', ()))
amount._edit(
# XXX If they are several cells, we may have duplicate references.
reference=self.getReference(),
quantity=quantity,
# XXX Are title, int_index and description useful ??
title=self.getTitle(),
int_index=self.getIntIndex(),
......@@ -295,52 +292,32 @@ class AmountGeneratorMixin:
# We hope here that rounding is sufficient at line level
amount = getRoundingProxy(amount, context=self)
result.append(amount)
for base_application, property_dict in value_amount_aggregate.iteritems():
# property_dict should include
# base_contribution_list - needed to produce reports with
# getTotalPrice
# quantity - quantity in component in MRP, (what else XXX)
# price - empty (like in Transformation) price of a product
# (ex. a Stamp) or tax ratio (ie. price per value units)
# XXX Why price ? What about efficiency ?
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
# Contribute
base_contribution_set = property_dict['base_contribution_set']
if base_contribution_set:
quantity *= (property_dict.get('price') or 1) / \
(property_dict.get('efficiency') or 1)
base_amount.updateCache(base_contribution_set, self)
for base_contribution in base_contribution_set:
base_amount[base_contribution] += quantity
is_mapped_value = isinstance(self, MappedValue)
# Each amount in amount_list creates a new amount to take into account
# We thus need to start with a loop on amount_list
for delivery_amount in amount_list:
if not is_mapped_value:
self = delivery_amount.asComposedDocument(amount_generator_type_list)
# XXX It should be possible to keep specific keys in base_amount dict.
# This can be done by a preference listing base_amount categories
# for which we want to accumulate quantities.
base_amount = {None: 1}
base_contribution_set = delivery_amount.getBaseContributionSet()
# Check that lines are sorted correctly
applied_base_amount_set = set()
# Check that lines with missing or duplicate int_index are independant
check_wrong_index_set = set()
# Browse recursively the amount generator lines and accumulate
# applicable values - now execute the method
accumulateAmountList(self)
tv = getTransactionalVariable()
# backup & restore existing cached value for reentrancy
original_cache = tv.get('amount_generator.getGeneratedAmountList')
try:
tv['amount_generator.getGeneratedAmountList'] = base_amount_cache = {}
for base_amount in base_amount_list:
delivery_amount = base_amount.getContext()
base_amount_cache[delivery_amount] = base_amount
if not is_mapped_value:
self = delivery_amount.asComposedDocument(amount_generator_type_list)
# Browse recursively the amount generator lines and accumulate
# applicable values - now execute the method
accumulateAmountList(self)
finally:
tv['amount_generator.getGeneratedAmountList'] = original_cache
return result
security.declareProtected(Permissions.AccessContentsInformation,
......
......@@ -33,9 +33,7 @@ import transaction
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from DateTime import DateTime
from Products.CMFCore.utils import getToolByName
from Products.ERP5Type.tests.utils import reindex
from Products.ERP5Type.tests.utils import createZODBPythonScript, reindex
class TestBPMMixin(ERP5TypeTestCase):
"""Skeletons for tests which depend on BPM"""
......@@ -60,9 +58,9 @@ class TestBPMMixin(ERP5TypeTestCase):
@reindex
def createCategories(self):
category_tool = getToolByName(self.portal, 'portal_categories')
category_tool = self.portal.portal_categories
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.normal_resource_use_category_list + \
self.invoicing_resource_use_category_list)
......
......@@ -57,6 +57,35 @@ class TestTradeModelLineMixin(TestBPMMixin, UserDict):
# Constants and variables shared by tests
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):
UserDict.__init__(self)
return TestBPMMixin.afterSetUp(self)
......@@ -731,13 +760,21 @@ class TestTradeModelLine(TestTradeModelLineMixin):
to say "discount 10 euros" or "pay more 10 euros" instead of saying "10%
discount from total"
"""
fixed_quantity = self.setBaseAmountQuantityMethod('fixed_quantity', """\
return lambda *args, **kw: 1""")
tax = self.createServiceTax()
trade_condition = self.createTradeCondition((), (
# create a model line with 100 euros
dict(reference='A', resource_value=tax, quantity=100, price=1),
# add a discount of 10 euros
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()
self.assertEqual([-10, 100], sorted(x.getTotalPrice() for x in amount_list))
......@@ -904,31 +941,27 @@ class TestTradeModelLine(TestTradeModelLineMixin):
and trade model line can works with appropriate context(delivery or
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()
trade_condition = self.createTradeCondition(self.createBusinessProcess())
# 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,
reference='TAX',
resource_value=tax,
base_application='base_amount/tax',
test_method_id='isDelivery',
target_delivery=True,
price=0.05)
# create an order.
resource_A = self.createResource('Product', title='A')
resource_B = self.createResource('Product', title='B')
order = self.createOrder(trade_condition)
base_contribution_list = 'base_amount/tax', 'base_amount/extra_fee'
order.setBaseContributionList(base_contribution_list)
base_contribution_list = 'base_amount/tax', bounded_fee
kw = {'portal_type': self.order_line_portal_type,
'base_contribution_list': base_contribution_list}
order_line_1 = order.newContent(price=1000, quantity=1,
......@@ -939,7 +972,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
self.assertEqual([75], [x.getTotalPrice() for x in amount_list])
# change target level to `movement`.
tml.setTestMethodId('isMovement')
tml.setTargetDelivery(False)
amount_list = order.getGeneratedAmountList()
self.assertEqual([25, 50], sorted(x.getTotalPrice() for x in amount_list))
......@@ -948,32 +981,21 @@ class TestTradeModelLine(TestTradeModelLineMixin):
extra_fee_a = self.createTradeModelLine(trade_condition,
reference='EXTRA_FEE_A',
resource_value=tax,
base_application='base_amount/extra_fee',
test_method_id='isMovement',
base_application=bounded_fee,
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
# to all movements by force.
extra_fee_b = self.createTradeModelLine(trade_condition,
reference='EXTRA_FEE_B',
resource_value=tax,
test_method_id='isMovement',
base_application=fixed_quantity,
price=1)
# for delivery level
discount = self.createTradeModelLine(trade_condition,
reference='DISCOUNT_B',
resource_value=tax,
test_method_id='isDelivery',
base_application=fixed_quantity,
target_delivery=True,
quantity=10, price=-1)
transaction.commit() # flush transactional cache
......@@ -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
self.assertEqual(sorted(expected_tax),
sorted(x.getTotalPrice() for x in amount_list))
# Change target level
extra_fee_a.setTestMethodId('isDelivery')
extra_fee_b.setTestMethodId('isDelivery')
extra_fee_a.setTargetDelivery(True)
extra_fee_b.setTargetDelivery(True)
amount_list = order.getAggregatedAmountList()
expected_tax = 1000*0.05 + 500*0.05, 800*0.2, 1, -10
self.assertEqual(sorted(expected_tax),
sorted(x.getTotalPrice() for x in amount_list))
@expectedFailure
def test_tradeModelLineWithRounding(self):
"""
Test if trade model line works with rounding.
......@@ -1004,9 +1024,10 @@ if base_application == 'base_amount/extra_fee':
# create a model line and set target level to `delivery`
tax = self.createTradeModelLine(trade_condition,
reference='TAX',
base_application_list=['base_amount/tax'],
base_contribution_list=['base_amount/total_tax'])
tax.edit(price=0.05, target_level=TARGET_LEVEL_DELIVERY)
base_application='base_amount/tax',
base_contribution='base_amount/total_tax',
price=0.05,
target_delivery=True)
# create a rounding model for tax
rounding_model = self.portal.portal_roundings.newContent(portal_type='Rounding Model')
......@@ -1024,62 +1045,51 @@ if base_application == 'base_amount/extra_fee':
order_line_1 = order.newContent(portal_type=self.order_line_portal_type,
price=3333, quantity=1,
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,
price=171, quantity=1,
resource_value=resource_B,
base_contribution_list=['base_amount/tax'])
base_contribution='base_amount/tax')
transaction.commit()
self.tic()
# check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list))
self.assertEqual(set([order_line_1, order_line_2]),
set(amount_list[0].getCausalityValueList()))
self.assertEqual((3333+171)*0.05, amount_list[0].getTotalPrice())
amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual((3333+171)*0.05, amount.getTotalPrice()) # 175.2
# check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True)
self.assertEqual(1, len(amount_list))
self.assertEqual(set([order_line_1, order_line_2]),
set(amount_list[0].getCausalityValueList()))
self.assertEqual(175, amount_list[0].getTotalPrice())
amount, = order.getAggregatedAmountList(rounding=True)
self.assertEqual(175, amount.getTotalPrice())
# change tax trade model line to `movement` level
tax.edit(target_level=TARGET_LEVEL_MOVEMENT)
tax.setTargetDelivery(False)
def getTotalAmount(amount_list):
result = 0
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()
return result
# check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list))
self.assertEqual(3333*0.05+171*0.05, getTotalAmount(amount_list))
amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(3333*0.05+171*0.05, amount.getTotalPrice()) # 175.2
# check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True)
self.assertEqual(2, len(amount_list))
expectedFailure(order.getAggregatedAmountList)(rounding=True)
self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(174, getTotalAmount(amount_list))
# check getAggregatedAmountList result of each movement
# order line 1
amount_list = order_line_1.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list))
self.assertEqual(3333*0.05, amount_list[0].getTotalPrice())
amount_list = order_line_1.getAggregatedAmountList(rounding=True)
self.assertEqual(1, len(amount_list))
self.assertEqual(166, amount_list[0].getTotalPrice())
amount, = order_line_1.getAggregatedAmountList(rounding=False)
self.assertEqual(3333*0.05, amount.getTotalPrice()) # 166.65
amount, = order_line_1.getAggregatedAmountList(rounding=True)
self.assertEqual(166, amount.getTotalPrice())
# order line 2
amount_list = order_line_2.getAggregatedAmountList(rounding=False)
self.assertEqual(1, len(amount_list))
self.assertEqual(171*0.05, amount_list[0].getTotalPrice())
amount_list = order_line_2.getAggregatedAmountList(rounding=True)
self.assertEqual(1, len(amount_list))
self.assertEqual(8, amount_list[0].getTotalPrice())
amount, = order_line_2.getAggregatedAmountList(rounding=False)
self.assertEqual(171*0.05, amount.getTotalPrice()) # 8.55
amount, = order_line_2.getAggregatedAmountList(rounding=True)
self.assertEqual(8, amount.getTotalPrice())
# change rounding model definition
rounding_model.setDecimalRoundingOption('ROUND_UP')
......@@ -1093,15 +1103,14 @@ if base_application == 'base_amount/extra_fee':
self.tic()
# check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list))
self.assertEqual(3.3333*3333*0.05+171*0.05, getTotalAmount(amount_list))
amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(3.3333*3333*0.05+171*0.05, amount.getTotalPrice())
# check the result with rounding
# 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
# (1* 171) * 0.05)"
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))
# create a rounding model to round quantity property of order line
......@@ -1119,7 +1128,7 @@ if base_application == 'base_amount/extra_fee':
amount_list = order.getAggregatedAmountList(rounding=True)
# 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)"
self.assertEqual(2, len(amount_list))
self.assertEqual(2, len(amount_list)) # XXX 1 or 2 ???
self.assertEqual(509, getTotalAmount(amount_list))
# create a rounding model to round price property of order line
......@@ -1140,14 +1149,13 @@ if base_application == 'base_amount/extra_fee':
self.tic()
# check the result without rounding
amount_list = order.getAggregatedAmountList(rounding=False)
self.assertEqual(2, len(amount_list))
self.assertEqual(3.3333*3333*0.05+171.1234*0.05, getTotalAmount(amount_list))
amount, = order.getAggregatedAmountList(rounding=False)
self.assertEqual(3.3333*3333*0.05+171.1234*0.05, amount.getTotalPrice())
# check the result with rounding
amount_list = order.getAggregatedAmountList(rounding=True)
# The expression should be "round_down(3.3333) * round_up(3333) * 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))
def test_tradeModelLineWithEmptyBaseContributionMovement(self):
......
......@@ -659,23 +659,31 @@ def registerBaseCategories(property_sheet):
base_category_dict[bc] = 1
def importLocalInterface(module_id, path = None, is_erp5_type=False):
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:
class_id = "I" + convertToUpperCase(module_id)
if not is_erp5_type:
def provides(class_id):
# Create interface getter
accessor_name = 'provides' + class_id
setattr(BaseClass, accessor_name, lambda self: self.provides(class_id))
BaseClass.security.declarePublic(accessor_name)
class_id = "I" + convertToUpperCase(module_id)
if 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)
import Products.ERP5Type.interfaces
setattr(Products.ERP5Type.interfaces, class_id, getattr(module, class_id))
finally:
f.close()
# Create interface getter
accessor_name = 'provides' + class_id
setattr(BaseClass, accessor_name, lambda self: self.provides(class_id))
BaseClass.security.declarePublic(accessor_name)
finally:
f.close()
from zope.interface import Interface
from Products.ERP5Type import interfaces
InterfaceClass = type(Interface)
for k, v in module.__dict__.iteritems():
if type(v) is InterfaceClass and v is not Interface:
setattr(interfaces, k, v)
provides(class_id)
def importLocalConstraint(class_id, path = None):
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