From 4407076fbaf616e2546d468a3b0665e69c7c647c Mon Sep 17 00:00:00 2001 From: Jean-Paul Smets <jp@nexedi.com> Date: Mon, 1 Mar 2010 15:01:10 +0000 Subject: [PATCH] First working version for transformations (using new transformation code which is not yet committed). git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@33187 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/mixin/amount_generator.py | 86 ++++-- product/ERP5/mixin/variated.py | 394 +++++++++++++++++++++++++ product/ERP5/mixin/variation.py | 53 ++++ 3 files changed, 515 insertions(+), 18 deletions(-) create mode 100644 product/ERP5/mixin/variated.py create mode 100644 product/ERP5/mixin/variation.py diff --git a/product/ERP5/mixin/amount_generator.py b/product/ERP5/mixin/amount_generator.py index 451857255e..51dc863c68 100644 --- a/product/ERP5/mixin/amount_generator.py +++ b/product/ERP5/mixin/amount_generator.py @@ -27,6 +27,7 @@ ############################################################################## import zope.interface +from zLOG import LOG from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions, interfaces from Products.ERP5.Document.Amount import Amount @@ -34,8 +35,15 @@ from Products.ERP5.Document.Amount import Amount class AmountGeneratorMixin: """ This class provides a generic implementation of IAmountGenerator. + It is used by Transformation, Trade Model, Paysheet, etc. It is + designed to support about any transformation process based + on IMappedValue interface. The key idea is that the Amount Generator + Lines and Cell provide either directly or through acquisition the + methods 'getMappedValuePropertyList' and 'getMappedValueBaseCategoryList' + to gather the properties and categories to copy from the model + to the generated amounts. - NOTE: this is an early prototype of implementation + NOTE: this is an first prototype of implementation """ # Declarative security @@ -47,27 +55,48 @@ class AmountGeneratorMixin: def _getGlobalPropertyDict(self, context, amount_list=None, rounding=False): """ - This method can be overridden to define global - properties involved in trade model line calculation + This method must be overridden to define global + properties involved in trade model line or transformation calculation + + TODO: + default implementation could use type based method """ raise NotImplementedError + # Example of return value return { - 'delivery': 1, - 'employee': 100, + 'delivery': 1, # Sets the base_amount 'delivery' to 1 + # so that it is possible to create models based + # on the number of deliveries (instead of quantity) + 'employee': 100, # Sets the base_amount 'employee' to 100 + # so that it is possible to create models based + # on the number of employee (instead of quantity) } def _getAmountPropertyDict(self, amount, amount_list=None, rounding=False): """ - This method can be overridden to define local - properties involved in trade model line calculation + This method must be overridden to define per amount local + properties involved in trade model line or transformation calculation + + TODO: + default implementation could use type based method """ raise NotImplementedError + # Example of return value return dict( price=amount.getPrice(), + # Sets the base_amount 'price' to the price + # This base_amount often uses another name though quantity=amount.getQuantity(), + # Sets the base_amount 'quantity' to the quantity + # This base_amount often uses another name though unit=(amount.getQuantityUnit() == 'unit') * amount.getQuantity(), + # Sets the base_amount 'unit' to the number of units + # so that it is possible to create models based + # on the number of units ton=(amount.getQuantityUnit() == 'ton') * amount.getQuantity(), - # more base applications could be set here + # Sets the base_amount 'ton' to the weight in tons + # so that it is possible to create models based + # on the weight in tons ) security.declareProtected(Permissions.AccessContentsInformation, 'getGeneratedAmountList') @@ -82,6 +111,10 @@ class AmountGeneratorMixin: - is rounding really well supported (ie. before and after aggregation) very likely not - proxying before or after must be decided """ + # It is the only place we can import this + from Products.ERP5Type.Document import newTempAmount + portal = self.getPortalObject() + # Initialize base_amount global properties (which can be modified # during the calculation process) base_amount = self._getGlobalPropertyDict(context, amount_list=amount_list, rounding=rounding) @@ -104,13 +137,13 @@ class AmountGeneratorMixin: # 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 amount in amount_list: + for delivery_amount in amount_list: # Initialize base_amount with per amount properties - amount_propert_dict = self._getAmountPropertyDict(amount, amount_list=amount_list, rounding=rounding) + amount_propert_dict = self._getAmountPropertyDict(delivery_amount, amount_list=amount_list, rounding=rounding) base_amount.update(amount_propert_dict) # Initialize base_amount with total_price for each amount applications - for application in amount.getBaseApplicationList(): # Acquired from Resource + for application in delivery_amount.getBaseApplicationList(): # Acquired from Resource base_amount[application] = amount.getTotalPrice() # Browse recursively the trade model and accumulate @@ -119,7 +152,7 @@ class AmountGeneratorMixin: amount_generator_line_list = amount_generator_line.contentValues(portal_type=self.getPortalAmountGeneratorLineTypeList()) # Recursively feed base_amount if len(amount_generator_line_list): - amount_generator_line_list = amount_generator_line_list.sort(key=lambda x: x.getIntIndex()) + amount_generator_line_list.sort(key=lambda x: x.getIntIndex()) for amount_generator_line in amount_generator_line_list: accumulateAmountList(amount_generator_line) return @@ -133,21 +166,27 @@ class AmountGeneratorMixin: resource_amount_aggregate = {} # aggregates final line information value_amount_aggregate = {} # aggregates intermediate line information for amount_generator_cell in amount_generator_cell_list: - if amount_generator_cell.test(amount): # XXX-JPS getTargetLevel not supported + if amount_generator_cell.test(delivery_amount): # XXX-JPS getTargetLevel not supported # Case 1: the cell defines a final amount of resource - if amount_generator_cell.getResource(): + if amount_generator_cell.getResource() and\ + getattr(amount_generator_cell, 'getBaseApplication', None) is not None: # We must aggregate per resource, variation key = (amount_generator_cell.getResource(), amount_generator_cell.getVariationText()) # Variation UID, Hash ? resource_amount_aggregate.setdefault(key, {}) # Then collect the mapped properties (resource, quantity, net_converted_quantity, base_contribution_list, base_application, etc.) for property_key in amount_generator_cell.getMappedValuePropertyList(): - # Handling of property lists ? XXX? + # XXX-JPS Make sure handling of list properties can be handled resource_amount_aggregate[key][property_key] = amount_generator_cell.getProperty(property_key) resource_amount_aggregate[key]['category_list'] = amount_generator_cell.getCategoryMembershipList( amount_generator_cell.getMappedValueBaseCategoryList()) + resource_amount_aggregate[key]['resource'] = amount_generator_cell.getResource() + # For final amounts, base_application and id MUST be defined + resource_amount_aggregate[key]['base_application'] = amount_generator_cell.getBaseApplication() # Required + resource_amount_aggregate[key]['id'] = amount_generator_cell.getRelativeUrl().replace('/', '_') # Case 2: the cell defines a temporary calculation line - else: + elif getattr(amount_generator_cell, 'getBaseContributionList', None) is not None: # We must aggregate per base_application + # Therefore, base_application MUST be defined key = amount_generator_cell.getBaseApplication() value_amount_aggregate.setdefault(key, {}) # Then collect the mapped properties @@ -155,6 +194,8 @@ class AmountGeneratorMixin: value_amount_aggregate[key][property_key] = amount_generator_cell.getProperty(property_key) value_amount_aggregate[key]['category_list'] = amount_generator_cell.getCategoryMembershipList( amount_generator_cell.getMappedValueBaseCategoryList()) + # For intermediate calculations, base_contribution_list MUST be defined + value_amount_aggregate[key]['base_contribution_list'] = amount_generator_cell.getBaseContributionList() # Required if resource_amount_aggregate: for key, property_dict in resource_amount_aggregate.items(): resource, variation_text = key @@ -164,6 +205,7 @@ class AmountGeneratorMixin: else: category_list = None base_application = property_dict['base_application'] + del property_dict['base_application'] # property_dict should include # resource - VAT service or a Component in MRP # quantity - quantity in component in MRP, (what else XXX) @@ -177,12 +219,16 @@ class AmountGeneratorMixin: # values converted to the default management unit # If not quantity is provided, we consider that the value is 1.0 (XXX is it OK ?) property_dict['quantity'] = base_amount[amount_generator_line.getBaseApplication()] * \ - (property_dict.get('net_converted_quantity', property_dict.get('quantity')), 1.0) + (property_dict.get('net_converted_quantity', property_dict.get('quantity', 1.0))) + # This sounds wrong if cell has getBaseApplication() + # We should not keep net_converted_quantity if property_dict.get('net_converted_quantity', None) is not None: del property_dict['net_converted_quantity'] # Create an Amount object - amount = Amount() # XXX-JPS we could use a movement for safety + id = property_dict['id'] + del property_dict['id'] + amount = newTempAmount(portal, id) # XXX-JPS Could we use a movement for safety ? if category_list: amount._setCategoryList(category_list) amount._edit(**property_dict) if rounding: @@ -221,7 +267,11 @@ class AmountGeneratorMixin: Implementation of a generic transformation algorith which is applicable to payroll, tax generation and BOMs. Return the list of amounts with aggregation. + + TODO: + - make working sample code """ raise NotImplementedError + # Example of return code result = self.getGeneratedAmountList(context, movement_list=movement_list, rounding=rounding) return SomeMovementGroup(result) diff --git a/product/ERP5/mixin/variated.py b/product/ERP5/mixin/variated.py new file mode 100644 index 0000000000..8f9878f101 --- /dev/null +++ b/product/ERP5/mixin/variated.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2002-2010 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@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. +# +############################################################################## + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type.Globals import InitializeClass +from Products.CMFCore.utils import getToolByName + +from Products.ERP5Type import Context, interfaces, Permissions +from Products.ERP5Type.Base import Base +from Products.CMFCategory.Renderer import Renderer + +from warnings import warn +from zope.interface import implements + +class VariatedMixin(Base): + """ + Variated is a mix-in class for all classes which implement + the Variated Interface. + + A Variable object is an object which can variate + according to multiple dimensions. Variable objects include: + + - a Resource instance + + - an Amount instance (a Movement, a DeliveryLine, etc.) + + - an Item + + - a TransformedResource instance + """ + + # Declarative security + security = ClassSecurityInfo() + + # Declarative interfaces + implements(interfaces.IVariated) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationBaseCategoryList') + def getVariationBaseCategoryList(self, omit_optional_variation=0, + omit_option_base_category=None, omit_individual_variation=0): + """ + Return the list of variation base category. + If omit_optional_variation==1, do not include base category + considered as option (ex: industrial_phase). + """ + #XXX backwards compatibility + if omit_option_base_category is not None: + warn("Please use omit_optional_variation instead of"\ + " omit_option_base_category.", DeprecationWarning) + omit_optional_variation = omit_option_base_category + + vbcl = self._baseGetVariationBaseCategoryList() + if omit_optional_variation == 1: + # XXX First implementation + # option base category list is a portal method, until the creation + # of a good API. + option_base_category_list = self.getPortalOptionBaseCategoryList() + vbcl = [x for x in vbcl if x not in option_base_category_list] + else: + vbcl.extend(self.getOptionalVariationBaseCategoryList()) + + if omit_individual_variation == 0: + vbcl.extend(self.getIndividualVariationBaseCategoryList()) + + return vbcl + + security.declareProtected(Permissions.AccessContentsInformation, + '_getVariationCategoryList') + def _getVariationCategoryList(self, base_category_list = ()): + if base_category_list is (): + base_category_list = self.getVariationBaseCategoryList() +# base_category_list = self.getVariationRangeBaseCategoryList() + return self.getAcquiredCategoryMembershipList(base_category_list, base=1) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationCategoryList') + def getVariationCategoryList(self, base_category_list=(), + omit_optional_variation=0, omit_option_base_category=None): + """ + Returns the list of possible variations + """ + #XXX backwards compatibility + if omit_option_base_category is not None: + warn("Please use omit_optional_variation instead of"\ + " omit_option_base_category.", DeprecationWarning) + omit_optional_variation = omit_option_base_category + + return self._getVariationCategoryList( + base_category_list=base_category_list) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationCategoryItemList') + def getVariationCategoryItemList(self, base_category_list=(), base=1, + display_id='logical_path', display_base_category=1, + current_category=None, omit_optional_variation=0, + omit_option_base_category=None, **kw): + """ + Returns the list of possible variations + """ + #XXX backwards compatibility + if omit_option_base_category is not None: + warn("Please use omit_optional_variation instead of"\ + " omit_option_base_category.", DeprecationWarning) + omit_optional_variation = omit_option_base_category + + variation_category_item_list = [] + if current_category is not None: + variation_category_item_list.append((current_category,current_category)) + + if base_category_list is (): + base_category_list = self.getVariationBaseCategoryList() + if omit_optional_variation == 1: + base_category_list = [x for x in base_category_list if x not in + self.getPortalOptionBaseCategoryList()] + # Prepare 2 rendering + portal_categories = self.portal_categories + for base_category in base_category_list: + variation_category_list = self._getVariationCategoryList( + base_category_list=[base_category]) + + category_list = [] + object_list = [] + for variation_category_path in variation_category_list: + try: + variation_category = portal_categories.resolveCategory( + variation_category_path) + var_cat_portal_type = variation_category.getPortalType() + except AttributeError: + variation_category_item_list.append((variation_category_path, + variation_category_path)) + else: + if var_cat_portal_type != 'Category': + object_list.append(variation_category) + else: + category_list.append(variation_category) + # Render categories + variation_category_item_list.extend(Renderer( + display_base_category=display_base_category, + display_none_category=0, base=base, + current_category=current_category, + display_id=display_id, **kw).\ + render(category_list)) + # Render the others + variation_category_item_list.extend(Renderer( + base_category=base_category, + display_base_category=display_base_category, + display_none_category=0, base=base, + current_category=current_category, + display_id='title', **kw).\ + render(object_list)) + return variation_category_item_list + + # XXX Is it used ? +# def getVariationCategoryTitleOrIdItemList(self, base_category_list=(), +# base=1, **kw): +# """ +# Returns a list of tuples by parsing recursively all categories in a +# given list of base categories. Uses getTitleOrId as method +# """ +# return self.getVariationCategoryItemList( +# display_id='title_or_id', +# base_category_list=base_category_list, base=base, **kw) + + security.declareProtected(Permissions.ModifyPortalContent, + '_setVariationCategoryList') + def _setVariationCategoryList(self, node_list, base_category_list=()): + if base_category_list is (): + base_category_list = self.getVariationBaseCategoryList() + self._setCategoryMembership(base_category_list,node_list,base=1) + + security.declareProtected(Permissions.ModifyPortalContent, + 'setVariationCategoryList') + def setVariationCategoryList(self, node_list, base_category_list=()): + self._setVariationCategoryList(node_list, + base_category_list=base_category_list) + self.reindexObject() + + # Range + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationRangeBaseCategoryList') + def getVariationRangeBaseCategoryList(self): + """ + Returns possible variation base_category ids. + """ + # Get a portal method which defines a list of + # variation base category + return self.getPortalVariationBaseCategoryList() + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationRangeBaseCategoryItemList') + def getVariationRangeBaseCategoryItemList(self, base=1, + display_id='getTitle', + current_category=None): + """ + Returns possible variations of the resource + as a list of tuples (id, title). This is mostly + useful in ERP5Form instances to generate selection + menus. + """ + return self.portal_categories.getItemList( + self.getVariationBaseCategoryList()) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationBaseCategoryItemList') + def getVariationBaseCategoryItemList(self, display_id='title_or_id', + omit_optional_variation=0, omit_option_base_category=None, + omit_individual_variation=0): + """ + Returns base category of the resource + as a list of tuples (title, id). This is mostly + useful in ERP5Form instances to generate selection + menus. + """ + #XXX backwards compatibility + if omit_option_base_category is not None: + warn("Please use omit_optional_variation instead of"\ + " omit_option_base_category.", DeprecationWarning) + omit_optional_variation = omit_option_base_category + + variation_base_category_list = self.getVariationBaseCategoryList( + omit_optional_variation=omit_optional_variation, + omit_individual_variation=omit_individual_variation) + result = [] + for base_category in variation_base_category_list: + bc = self.portal_categories.resolveCategory(base_category) + result.extend(Renderer(display_base_category=0, + display_none_category=0, base=1, + display_id=display_id).render([bc])) + return result + + # Methods for matrix UI widgets + # XXX FIXME Those method are depreciated. + # We now use _asCellRange scripts. + security.declareProtected(Permissions.AccessContentsInformation, + 'getLineVariationRangeCategoryItemList') + def getLineVariationRangeCategoryItemList(self): + """ + Returns possible variations in line + """ + try: + resource = self.getDefaultResourceValue() + except AttributeError: + resource = None + if resource is not None: + clist = resource.getVariationRangeCategoryItemList( + base_category_list=self.getVariationBaseCategoryLine(), + root=0) + else: + clist = [(None,None)] + return clist + + security.declareProtected(Permissions.AccessContentsInformation, + 'getColumnVariationRangeCategoryItemList') + def getColumnVariationRangeCategoryItemList(self): + """ + Returns possible variations in column + """ + try: + resource = self.getDefaultResourceValue() + except AttributeError: + resource = None + if resource is not None: + clist = resource.getVariationRangeCategoryItemList(base_category_list = + self.getVariationBaseCategoryColumn(), root=0) + else: + clist = [(None,None)] + return clist + + security.declareProtected(Permissions.AccessContentsInformation, + 'getTabVariationRangeCategoryItemList') + def getTabVariationRangeCategoryItemList(self): + """ + Returns possible variations in tab + """ + try: + resource = self.getDefaultResourceValue() + except AttributeError: + resource = None + if resource is not None: + clist = resource.getVariationRangeCategoryItemList(base_category_list = + self.getVariationBaseCategoryTabList(), root=0) + else: + clist = [(None,None)] + return clist + + # Help + security.declareProtected(Permissions.AccessContentsInformation, + 'getMatrixVariationRangeBaseCategoryList') + def getMatrixVariationRangeBaseCategoryList(self): + """ + Return base categories used in the matrix + """ + line_bc= self.getVariationBaseCategoryLine() + column_bc = self.getVariationBaseCategoryColumn() + # We need to copy values first + tab_bc = list(self.getVariationBaseCategoryTabList()) + result = tab_bc + if line_bc is not None and line_bc is not '': + result += [line_bc] + if column_bc is not None and column_bc is not '': + result += [column_bc] + return result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationRangeCategoryItemList') + def getVariationRangeCategoryItemList(self, base_category_list=(), base=1, + root=1, + display_method_id='getCategoryChildLogicalPathItemList', + display_base_category=1, + current_category=None, **kw): + """ + Returns possible variations + => [(display, value)] + """ + result = [] + if base_category_list is (): + base_category_list = self.getVariationBaseCategoryList() + elif type(base_category_list) is type('a'): + base_category_list = (base_category_list, ) + + traverse = getToolByName(self, 'portal_categories').unrestrictedTraverse + # Render categories + for base_category in base_category_list: + result += getattr(traverse(base_category), display_method_id)( + base=base, + display_base_category=display_base_category, + display_none_category=0, **kw) + # Return result + return result + + security.declareProtected(Permissions.AccessContentsInformation, + 'getVariationRangeCategoryList') + def getVariationRangeCategoryList(self, base_category_list=(), base=1, + root=1, current_category=None, + omit_individual_variation=0): + """ + Returns the range of acceptable categories + """ + vrcil = self.getVariationRangeCategoryItemList( + base_category_list=base_category_list, + base=base, root=root, + current_category=current_category, + omit_individual_variation=omit_individual_variation) + # display is on left + return [x[1] for x in vrcil] + + # Context related methods + security.declarePublic('newVariationValue') + def newVariationValue(self, context=None, REQUEST=None, **kw): + # PERFORMANCE ISSUE + from Products.ERP5.VariationValue import newVariationValue + if context is None: + return newVariationValue(REQUEST=REQUEST, **kw) + else: + return newVariationValue(context=context, REQUEST=REQUEST, **kw) + + # Provide a string representation of variations + security.declarePublic('getVariationText') + def getVariationText(self): + """ + Provide a string representation of variation + """ + category_list = list(self.getVariationCategoryList()) + category_list.sort() + return '\n'.join(category_list) + +InitializeClass(VariatedMixin) diff --git a/product/ERP5/mixin/variation.py b/product/ERP5/mixin/variation.py new file mode 100644 index 0000000000..70306dff68 --- /dev/null +++ b/product/ERP5/mixin/variation.py @@ -0,0 +1,53 @@ +# -*- 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 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. +# +############################################################################## + +import zope.interface +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, interfaces +from Products.ERP5.Document.Amount import Amount + +class VariationMixin: + """ + + Although MappedValue are supposed to be independent of any + design choice, we have to implement them as subclasses of + Amount in order to make sure they provide a complete + variation interface. In particular, we want to be able + to call getVariationValue / setVariationValue on a + MappedValue. + (XXX - NO MORE) + + XXX - Amount should be remove from here + + + Interesting Idea: properties and categories of the mapped value + (not of the predicate) could be handled through additional matrix + dimensions rather than through ad-hoc definition. + """ + + \ No newline at end of file -- 2.30.9