Commit a762c628 authored by Yusei Tahara's avatar Yusei Tahara

Add rounding system(tool and model). Insert rounding into TradeModelLine.getAggregatedAmountList.


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@30865 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 6c03bc28
##############################################################################
#
# Copyright (c) 2009 Nexedi KK and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees 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., 51 Franklin Street - Fifth Floor, Boston, MA
# 02110-1301, USA.
#
##############################################################################
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import PropertySheet, Permissions
from Products.ERP5.Document.Predicate import Predicate
from Products.ERP5Type.Utils import UpperCase
class RoundingModel(Predicate):
"""
A Rounding Model class which defines rounding rule.
"""
meta_type = 'ERP5 Rounding Model'
portal_type = 'Rounding Model'
add_permission = Permissions.AddPortalContent
security = ClassSecurityInfo()
property_sheets = (PropertySheet.Base,
PropertySheet.SimpleItem,
PropertySheet.XMLObject,
PropertySheet.CategoryCore,
PropertySheet.DublinCore,
PropertySheet.Predicate,
PropertySheet.SortIndex,
PropertySheet.RoundingModel,
)
security.declareProtected(Permissions.AccessContentsInformation, 'roundValue')
def roundValue(self, value):
"""
Return rounded value.
"""
if self.getRoundingMethodId() is not None:
rounding_method = getattr(self, 'RoundingModel_%s' % self.getRoundingMethodId(), None)
if rounding_method is None:
raise ValueError, 'Rounding method (%s) was not found.'
else:
from decimal import Decimal
from Products.ERP5.Tool.RoundingTool import ROUNDING_OPTION_DICT
decimal_rounding_option = self.getDecimalRoundingOption()
if (decimal_rounding_option is None or
decimal_rounding_option not in ROUNDING_OPTION_DICT):
raise ValueError, 'Decimal rounding option must be selected.'
def rounding_method(value, decimal_exponent):
return float(Decimal(str(value)).quantize(Decimal(decimal_exponent),
rounding=decimal_rounding_option))
return rounding_method(value, self.getDecimalExponent())
security.declareProtected(Permissions.AccessContentsInformation, 'getRoundingProxy')
def getRoundingProxy(self, document):
"""
Return a rounding proxy object which getter methods returns rounded
value by following the rounding model definition.
"""
rounding_model = self
rounded_property_getter_method_name_list = []
if isinstance(document, RoundingProxy):
temp_document = document._getOriginalDocument()
original_document = document
else:
from Products.ERP5Type import Document
if document.__class__.__name__ == 'TempDocument':
class_ = document.__class__.__bases__[0]
else:
class_ = document.__class__
constructor = getattr(Document, 'newTemp%s' % class_.__name__)
temp_document = constructor(document.getParentValue(), 'id')
temp_document.__dict__.update(document.__dict__)
original_document = temp_document
for property_id in rounding_model.getRoundedPropertyIdList():
getter_name = 'get%s' % UpperCase(property_id)
getter = getattr(temp_document,
getter_name, None)
setter_name = 'set%s' % UpperCase(property_id)
setter = getattr(temp_document,
setter_name, None)
if getter is not None and setter is not None:
# round the property value itself
setter(self.roundValue(getter()))
else:
# cannot round the property value so that the return value of getter
# will be rounded
rounded_property_getter_method_name_list.append(getter_name)
class _RoundingProxy(RoundingProxy):
def _getOriginalDocument(self):
if isinstance(original_document, RoundingProxy):
return original_document._editOriginalDocument()
else:
return original_document
def __getattr__(self, name):
attribute = getattr(original_document, name)
if getattr(attribute, 'DUMMY_ROUNDING_METHOD_MARK', None) is DUMMY_ROUNDING_METHOD_MARK:
return attribute
if name in rounded_property_getter_method_name_list:
def dummyMethod(*args, **kw):
return rounding_model.roundValue(attribute(*args, **kw))
dummyMethod.DUMMY_ROUNDING_METHOD_MARK = DUMMY_ROUNDING_METHOD_MARK
return dummyMethod
else:
return attribute
return _RoundingProxy()
DUMMY_ROUNDING_METHOD_MARK = object()
class RoundingProxy(object):
"""Super class of _RoundingProxy class defined above. Use this class for
isinstance method to check if object is a real instance or a rounding proxy
instance.
"""
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
############################################################################## ##############################################################################
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName
from Products.ERP5Type import Permissions, PropertySheet, interfaces from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.XMLMatrix import XMLMatrix from Products.ERP5Type.XMLMatrix import XMLMatrix
from Products.ERP5.Document.Amount import Amount from Products.ERP5.Document.Amount import Amount
...@@ -168,6 +169,14 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): ...@@ -168,6 +169,14 @@ class TradeModelLine(Predicate, XMLMatrix, Amount):
base_id='movement', rounding=False, **kw): base_id='movement', rounding=False, **kw):
from Products.ERP5Type.Document import newTempSimulationMovement from Products.ERP5Type.Document import newTempSimulationMovement
# Define rounding stuff
portal_roundings = getToolByName(self, 'portal_roundings', None)
# ROUNDING
if rounding:
movement_list = [portal_roundings.getRoundingProxy(movement, context=self)
for movement in movement_list]
aggregated_amount_list = AggregatedAmountList() aggregated_amount_list = AggregatedAmountList()
base_application_list = self.getBaseApplicationList() base_application_list = self.getBaseApplicationList()
...@@ -247,6 +256,7 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): ...@@ -247,6 +256,7 @@ class TradeModelLine(Predicate, XMLMatrix, Amount):
update = 0 update = 0
base_category_list = self.getVariationBaseCategoryList() base_category_list = self.getVariationBaseCategoryList()
# get cells categories cartesian product # get cells categories cartesian product
cell_key_list = self.getCellKeyList(base_id='movement') cell_key_list = self.getCellKeyList(base_id='movement')
if len(cell_key_list) > 0: if len(cell_key_list) > 0:
...@@ -260,6 +270,17 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): ...@@ -260,6 +270,17 @@ class TradeModelLine(Predicate, XMLMatrix, Amount):
cell_coordinates)) cell_coordinates))
tmp_movement = newTempSimulationMovement(self.getPortalObject(), tmp_movement = newTempSimulationMovement(self.getPortalObject(),
self_id) self_id)
# ROUNDING
if rounding:
# Once tmp_movement is replaced with the proxy, then the proxy
# object returns rounded value.
# For example, if rounding model is defined as
# rounded_property_id='total_price', then proxied
# tmp_movement.getTotalPrice() returns rounded result.
# If rounded_property_id='quantity', then
# tmp_movement.getQuantity() will be rounded.
tmp_movement = portal_roundings.getRoundingProxy(tmp_movement, context=self)
tmp_movement.edit( tmp_movement.edit(
variation_base_category_list = cell.getVariationBaseCategoryList(), variation_base_category_list = cell.getVariationBaseCategoryList(),
variation_category_list = cell.getVariationCategoryList(), variation_category_list = cell.getVariationCategoryList(),
...@@ -275,6 +296,12 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): ...@@ -275,6 +296,12 @@ class TradeModelLine(Predicate, XMLMatrix, Amount):
price = self.getPrice(), price = self.getPrice(),
**common_params **common_params
) )
# ROUNDING
if rounding:
# Replace temporary movement with rounding proxy so that target
# property value will be rounded.
tmp_movement = portal_roundings.getRoundingProxy(tmp_movement, context=self)
tmp_movement_list.append(tmp_movement) tmp_movement_list.append(tmp_movement)
modified = 0 modified = 0
for tmp_movement in tmp_movement_list: for tmp_movement in tmp_movement_list:
......
...@@ -42,7 +42,7 @@ class RoundingModel(DecimalOption): ...@@ -42,7 +42,7 @@ class RoundingModel(DecimalOption):
}, },
{ 'id' : 'rounded_property_id', { 'id' : 'rounded_property_id',
'description' : 'The property name which value is rounded. Note that some property is virtual, like total_price.', 'description' : 'The property name which value is rounded. Note that some property is virtual, like total_price.',
'type' : 'string', 'type' : 'tokens',
'mode' : 'w', 'mode' : 'w',
'default' : None, 'default' : None,
}, },
......
# -*- coding: utf-8 -*-
#############################################################################
#
# Copyright (c) 2009 Nexedi KK and Contributors. All Rights Reserved.
# Yusei TAHARA <yusei@nexedi.com>
#
# 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.Tool.BaseTool import BaseTool
from Products.ERP5.interfaces.rounding_tool import IRoundingTool
from decimal import (ROUND_DOWN, ROUND_UP, ROUND_CEILING, ROUND_FLOOR,
ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP)
ROUNDING_OPTION_DICT = {'ROUND_DOWN':ROUND_DOWN,
'ROUND_UP':ROUND_UP,
'ROUND_CEILING':ROUND_CEILING,
'ROUND_FLOOR':ROUND_FLOOR,
'ROUND_HALF_DOWN':ROUND_HALF_DOWN,
'ROUND_HALF_EVEN':ROUND_HALF_EVEN,
'ROUND_HALF_UP':ROUND_HALF_UP}
class RoundingTool(BaseTool):
"""Rounding Tool"""
id = 'portal_roundings'
title = 'Rounding Tool'
meta_type = 'ERP5 Rounding Tool'
portal_type = 'Rounding Tool'
zope.interface.implements(IRoundingTool)
security = ClassSecurityInfo()
security.declarePublic('findRoundingModel')
def findRoundingModelValueList(self, document, property_id=None, context=None):
"""
Return a list of matched rounding models for `document` which is ordered
by increasing distance from `context`.
"""
portal = self.getPortalObject()
parent_uid_list = [portal.portal_roundings.getUid()]
kw = {}
if context is not None:
current_document = context
while True:
if (current_document is None or current_document is portal or
not current_document.getUid() or
current_document.getUid() in parent_uid_list):
break
else:
parent_uid_list.append(current_document.getUid())
current_document = current_document.aq_parent
def sortMethod(document_a, document_b):
def score(document):
context_path = context.getPhysicalPath()
result = len(context_path)
for a, b in zip(context_path,
document.getPhysicalPath()):
if a==b:
result -= 1
else:
break
return result
return cmp(score(document_a), score(document_b))
kw['sort_method'] = sortMethod
result = portal.portal_domains.searchPredicateList(
context=document,
parent_uid=parent_uid_list,
portal_type='Rounding Model',
validation_state='validated',
**kw)
return result
security.declarePublic('getRoundingProxy')
def getRoundingProxy(self, document, context=None):
"""
Return a rounding proxy object which getter methods returns rounded
value by following matched rounding model definition.
"""
target_object = document
for rounding_model in self.findRoundingModelValueList(document, context=context):
target_object = rounding_model.getRoundingProxy(target_object)
return target_object
security.declarePublic('getDecimalRoundingOptionList')
def getDecimalRoundingOptionItemList(self):
"""
Return the possible decimal rounding option item list which is provided
by python standard decimal module.
"""
return ROUNDING_OPTION_DICT.items()
...@@ -49,7 +49,7 @@ from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\ ...@@ -49,7 +49,7 @@ from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\
TestTool, DomainTool, AlarmTool, OrderTool, DeliveryTool,\ TestTool, DomainTool, AlarmTool, OrderTool, DeliveryTool,\
TrashTool, ContributionTool, NotificationTool, PasswordTool,\ TrashTool, ContributionTool, NotificationTool, PasswordTool,\
GadgetTool, ContributionRegistryTool, IntrospectionTool,\ GadgetTool, ContributionRegistryTool, IntrospectionTool,\
AcknowledgementTool, SolverTool, ConversionTool AcknowledgementTool, SolverTool, ConversionTool, RoundingTool
import ERP5Site import ERP5Site
object_classes = ( ERP5Site.ERP5Site, object_classes = ( ERP5Site.ERP5Site,
) )
...@@ -73,6 +73,7 @@ portal_tools = ( CategoryTool.CategoryTool, ...@@ -73,6 +73,7 @@ portal_tools = ( CategoryTool.CategoryTool,
AcknowledgementTool.AcknowledgementTool, AcknowledgementTool.AcknowledgementTool,
SolverTool.SolverTool, SolverTool.SolverTool,
ConversionTool.ConversionTool, ConversionTool.ConversionTool,
RoundingTool.RoundingTool,
) )
content_classes = () content_classes = ()
content_constructors = () content_constructors = ()
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2009 Nexedi KK and Contributors. All Rights Reserved.
# Yusei Tahara <yusei@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees and support are strongly advised 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.
#
##############################################################################
from zope.interface import Interface
class IRoundable(Interface):
"""
Roundable interface
"""
def asRoundingProxy(context=None):
"""
Return a proxy object with getter methods which returns rounded value.
"""
...@@ -34,9 +34,9 @@ class IRoundingTool(Interface): ...@@ -34,9 +34,9 @@ class IRoundingTool(Interface):
Rounding tool interface Rounding tool interface
""" """
def findRoundingModel(document, property_id, context=None): def findRoundingModelValueList(document, property_id=None, context=None):
""" """
Find matched rounding model for context and property id. Find matched rounding models for context and property id.
Parameters: Parameters:
...@@ -44,18 +44,25 @@ class IRoundingTool(Interface): ...@@ -44,18 +44,25 @@ class IRoundingTool(Interface):
This is the object which contains value to be rounded. This is the object which contains value to be rounded.
property_id property_id
XXX I'm not quite sure if this is really necessary or not...
This indicates which property value is rounded. This indicates which property value is rounded.
context context
This indicates where lookup starts from. If this is None, then rounding tool itself This indicates where lookup starts from. If this is None, then rounding
is used. tool itself is used.
Example: Example:
temporary_movement is generated by getAggregatedAmountList from a set of movements temporary_movement is generated by getAggregatedAmountList from a set of
and represents total price with tax. trade_model_line contains a rounding model to movements and represents total price with tax. trade_model_line contains
be used here to round the total price with tax. a rounding model to be used here to round the total price with tax.
portal_roundings.findRoundingModel(temporary_movement, 'total_price', portal_roundings.findRoundingModel(temporary_movement, 'total_price',
context=trade_model_line) context=trade_model_line)
""" """
def getRoundingProxy(document, context=None):
"""
Find matched rounding models from context and return proxy object for
`document`. The proxy object returns rounded value through getters.
"""
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