diff --git a/product/ERP5/Document/RoundingModel.py b/product/ERP5/Document/RoundingModel.py new file mode 100644 index 0000000000000000000000000000000000000000..bf476b876360c2f50e2bb9be047f222c48fe82f5 --- /dev/null +++ b/product/ERP5/Document/RoundingModel.py @@ -0,0 +1,141 @@ +############################################################################## +# +# 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. + """ diff --git a/product/ERP5/Document/TradeModelLine.py b/product/ERP5/Document/TradeModelLine.py index 7f949b2106b2a4af8e8d8e003ff7cac76b9b1218..b101b884bd6abe64a4f8f8734ef610358eadc473 100644 --- a/product/ERP5/Document/TradeModelLine.py +++ b/product/ERP5/Document/TradeModelLine.py @@ -29,6 +29,7 @@ ############################################################################## 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 @@ -168,6 +169,14 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): base_id='movement', rounding=False, **kw): 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() base_application_list = self.getBaseApplicationList() @@ -247,6 +256,7 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): update = 0 base_category_list = self.getVariationBaseCategoryList() + # get cells categories cartesian product cell_key_list = self.getCellKeyList(base_id='movement') if len(cell_key_list) > 0: @@ -260,6 +270,17 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): cell_coordinates)) tmp_movement = newTempSimulationMovement(self.getPortalObject(), 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( variation_base_category_list = cell.getVariationBaseCategoryList(), variation_category_list = cell.getVariationCategoryList(), @@ -275,6 +296,12 @@ class TradeModelLine(Predicate, XMLMatrix, Amount): price = self.getPrice(), **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) modified = 0 for tmp_movement in tmp_movement_list: diff --git a/product/ERP5/PropertySheet/RoundingModel.py b/product/ERP5/PropertySheet/RoundingModel.py index c56cf29e721fe0dbeac83d36c3cd02d5b69923b4..ab11b966108e278ab0cec29185aacd1579e82569 100644 --- a/product/ERP5/PropertySheet/RoundingModel.py +++ b/product/ERP5/PropertySheet/RoundingModel.py @@ -42,7 +42,7 @@ class RoundingModel(DecimalOption): }, { 'id' : 'rounded_property_id', 'description' : 'The property name which value is rounded. Note that some property is virtual, like total_price.', - 'type' : 'string', + 'type' : 'tokens', 'mode' : 'w', 'default' : None, }, diff --git a/product/ERP5/Tool/RoundingTool.py b/product/ERP5/Tool/RoundingTool.py new file mode 100644 index 0000000000000000000000000000000000000000..b4a4e8e6ebd2eecb798b34fe41c15d0bc2d2a52e --- /dev/null +++ b/product/ERP5/Tool/RoundingTool.py @@ -0,0 +1,115 @@ +# -*- 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() diff --git a/product/ERP5/__init__.py b/product/ERP5/__init__.py index 6598a07dedaf02b683f8bf86bc3f140c598f8d65..507406da106da6ecee622224c746c4a4ef71057d 100644 --- a/product/ERP5/__init__.py +++ b/product/ERP5/__init__.py @@ -49,7 +49,7 @@ from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\ TestTool, DomainTool, AlarmTool, OrderTool, DeliveryTool,\ TrashTool, ContributionTool, NotificationTool, PasswordTool,\ GadgetTool, ContributionRegistryTool, IntrospectionTool,\ - AcknowledgementTool, SolverTool, ConversionTool + AcknowledgementTool, SolverTool, ConversionTool, RoundingTool import ERP5Site object_classes = ( ERP5Site.ERP5Site, ) @@ -73,6 +73,7 @@ portal_tools = ( CategoryTool.CategoryTool, AcknowledgementTool.AcknowledgementTool, SolverTool.SolverTool, ConversionTool.ConversionTool, + RoundingTool.RoundingTool, ) content_classes = () content_constructors = () @@ -107,4 +108,4 @@ def initialize( context ): # backward compatibility names XML = None -UI = None \ No newline at end of file +UI = None diff --git a/product/ERP5/interfaces/roundable.py b/product/ERP5/interfaces/roundable.py new file mode 100644 index 0000000000000000000000000000000000000000..d75bdcec53570ecb7417922bffe1766a565cda2a --- /dev/null +++ b/product/ERP5/interfaces/roundable.py @@ -0,0 +1,40 @@ +# -*- 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. + """ diff --git a/product/ERP5/interfaces/rounding_tool.py b/product/ERP5/interfaces/rounding_tool.py index a8216d65526ca74bd3d2185013538216f8434b64..a13b11b6fdd53589e392105516fd26720d0cfa4a 100644 --- a/product/ERP5/interfaces/rounding_tool.py +++ b/product/ERP5/interfaces/rounding_tool.py @@ -34,9 +34,9 @@ class IRoundingTool(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: @@ -44,18 +44,25 @@ class IRoundingTool(Interface): This is the object which contains value to be rounded. property_id + XXX I'm not quite sure if this is really necessary or not... This indicates which property value is rounded. context - This indicates where lookup starts from. If this is None, then rounding tool itself - is used. + This indicates where lookup starts from. If this is None, then rounding + tool itself is used. Example: - temporary_movement is generated by getAggregatedAmountList from a set of movements - and represents total price with tax. trade_model_line contains a rounding model to - be used here to round the total price with tax. + temporary_movement is generated by getAggregatedAmountList from a set of + movements and represents total price with tax. trade_model_line contains + a rounding model to be used here to round the total price with tax. portal_roundings.findRoundingModel(temporary_movement, 'total_price', 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. + """