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