From 9b9c75c34d4c6435ec3f5acddf892754b0ea86a9 Mon Sep 17 00:00:00 2001
From: Romain Courteaud <romain@nexedi.com>
Date: Thu, 7 Jul 2005 12:19:33 +0000
Subject: [PATCH] Rewrote the rule. Need to finish the date calculation.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@3421 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/TransformationRule.py   | 453 ++++++++----------
 .../Document/TransformationSourcingRule.py    | 273 +++++------
 2 files changed, 341 insertions(+), 385 deletions(-)

diff --git a/product/ERP5/Document/TransformationRule.py b/product/ERP5/Document/TransformationRule.py
index 84169757b7..dc91fb4c9b 100755
--- a/product/ERP5/Document/TransformationRule.py
+++ b/product/ERP5/Document/TransformationRule.py
@@ -1,7 +1,8 @@
 ##############################################################################
 #
-# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
 #                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Romain Courteaud <romain@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -32,6 +33,8 @@ from Products.CMFCore.utils import getToolByName
 
 from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
 from Products.ERP5.Document.Rule import Rule
+from Products.ERP5.Document.TransformationSourcingRule import\
+                                            TransformationSourcingRuleMixin
 
 from zLOG import LOG
 
@@ -40,91 +43,20 @@ class TransformationRule(Rule):
       Order Rule object make sure an Order in the similation
       is consistent with the real order
     """
-
     # CMF Type Definition
     meta_type = 'ERP5 Transformation Rule'
     portal_type = 'Transformation Rule'
-    add_permission = Permissions.AddPortalContent
-    isPortalContent = 1
-    isRADContent = 1
-
     # Declarative security
     security = ClassSecurityInfo()
     security.declareObjectProtected(Permissions.View)
-
     # Default Properties
     property_sheets = ( PropertySheet.Base
                       , PropertySheet.XMLObject
                       , PropertySheet.CategoryCore
                       , PropertySheet.DublinCore
                       )
-
-    # CMF Factory Type Information
-    factory_type_information = \
-      {    'id'             : portal_type
-         , 'meta_type'      : meta_type
-         , 'description'    : """\
-An ERP5 Rule..."""
-         , 'icon'           : 'rule_icon.gif'
-         , 'product'        : 'ERP5'
-         , 'factory'        : 'addTransformationRule'
-         , 'immediate_view' : 'rule_view'
-         , 'allow_discussion'     : 1
-         , 'allowed_content_types': ()
-         , 'filter_content_types' : 1
-         , 'global_allow'   : 1
-         , 'actions'        :
-        ( { 'id'            : 'view'
-          , 'name'          : 'View'
-          , 'category'      : 'object_view'
-          , 'action'        : 'rule_view'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'list'
-          , 'name'          : 'Object Contents'
-          , 'category'      : 'object_action'
-          , 'action'        : 'folder_contents'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'print'
-          , 'name'          : 'Print'
-          , 'category'      : 'object_print'
-          , 'action'        : 'rule_print'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'metadata'
-          , 'name'          : 'Metadata'
-          , 'category'      : 'object_view'
-          , 'action'        : 'metadata_edit'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'translate'
-          , 'name'          : 'Translate'
-          , 'category'      : 'object_action'
-          , 'action'        : 'translation_template_view'
-          , 'permissions'   : (
-              Permissions.TranslateContent, )
-          }
-        )
-      }
-
-    security.declareProtected(Permissions.ModifyPortalContent, 'constructNewAppliedRule')
-    def constructNewAppliedRule(self, context, id=None):
-      """
-        Creates a new applied rule which points to self
-      """
-      my_applied_rule = Rule.constructNewAppliedRule(self, context, id=id)
-      resource = context.getDefaultResourceValue()
-      # Find my related transformation
-      transformation = resource.getDefaultResourceRelatedValue(portal_type = ('Transformation',))
-      if transformation is not None:
-        #LOG('Transformation Causality', 0, str(transformation.getId()))
-        my_applied_rule.setCausalityValue(transformation )
-      return my_applied_rule
+    # Class variable 
+    simulation_movement_portal_type = "Simulation Movement"
 
     security.declareProtected(Permissions.AccessContentsInformation, 'test')
     def test(self, movement):
@@ -134,188 +66,224 @@ An ERP5 Rule..."""
       # Test if we must transform
       # The test should actually be based on nodes, paths
       # and capacities, which is not possible now
-      # so we just test if it is a "model" !
-      # and if it is being source from the workshop
-
-      #LOG('Test Transformation Rule', 0, '')
-
+      result = 1
       # Only apply to Order applied rule
       root_applied_rule = movement.getRootAppliedRule()
       root_rule = root_applied_rule.getSpecialiseValue()
-      if root_rule is None:
-        return 0
-      if root_rule.getPortalType() != "Order Rule":
-        return 0
-      
-      # Only apply to certain resources
-      resource = movement.getResourceValue()
-      if resource is None:
-        return 0
-      module = resource.aq_parent
-
-      if module.id == 'modele':
-        # This is the modele !
-        # We must also test the nodes
-        # if the source is a production node
-        source = movement.getSource()
-        if type(source) is type('a'):
-          if source.find('site/Piquage') >= 0 :
-            return 1
-          return 0
-      elif module.id == 'assortiment':
-        destination = movement.getDestination()
-        if type(destination) is type('a'):
-          if destination.find('site/Stock_PF/Gravelines') >= 0 :
-            source = movement.getSource()
-            if type(source) is type('a'):
-              if source.find('site/Stock_PF/Gravelines') >= 0 :
-                return 1
-
-      return 0
+      order = root_applied_rule.getCausalityValue()
+      root_movement = movement.getRootSimulationMovement()
+      # Test movement
+      if (root_rule is None) or\
+         (root_rule.getPortalType() != "Production Order Rule") or\
+         (order is None) or\
+         (movement.getResourceValue() is None) or\
+         (movement.getSourceValue() is None) or\
+         (movement.getResourceValue() != root_movement.getResourceValue()):
+         # We only produced what is asked on the Production Order
+           result = 0
+      else:
+        supply_chain = self.getSupplyChain(movement.getParent())
+        parent_supply_link = self.getCurrentSupplyLink(movement)
+        current_tranfo_link_list = supply_chain.\
+                       getPreviousProductionSupplyLinkList(parent_supply_link)
+        length = len(current_tranfo_link_list)
+        if length == 0:
+          result = 0
+        elif length > 1:
+          result = 0
+          # XXX FIXME: implementation needed
+          raise "TransformationRuleError",\
+                "TransformationRule not able to use multiple SupplyLink."
+      return result
 
     # Simulation workflow
     security.declareProtected(Permissions.ModifyPortalContent, 'expand')
     def expand(self, applied_rule, **kw):
       """
         Expands the current movement downward.
-
         -> new status -> expanded
-
         An applied rule can be expanded only if its parent movement
         is expanded.
       """
-      delivery_line_type = 'Simulation Movement'
-
-      # Get the order where we come from
-      my_transformation = applied_rule.getCausalityValue()
-
-      # No transformation defined
-      if my_transformation is None:
-        # Things stop here
-        applied_rule.diverge()
-        return
-
-      # Find production node
-      my_context_movement = applied_rule.getParent()
-      production_node = my_context_movement.getSource()
-      LOG('TransformationRule.expand my_context_movement.getPhysicalPath()',0,my_context_movement.getPhysicalPath())
-      LOG('TransformationRule.expand my_context_movement.getSource()',0,my_context_movement.getSource())
-      LOG('TransformationRule.expand my_context_movement.getTargetSource()',0,my_context_movement.getTargetSource())
-      production_section = my_context_movement.getSourceSection()
-      # Generate production and consumption lines
-      my_quantity = my_context_movement.getTargetQuantity()
-      #LOG('Transformation', 0, str(my_transformation))
-      # We used to call this with context = my_context_movement
-      # but it still has some issue which need to be fixed XXX As
-      # a temp solution, we use the dict based API, but it is not general enough
-      # and will causse errors on countinuous variations
-      # suspected bug cause is probably related to the use of REQUEST where it should not
-      # ie. we acquire some unwanted context
-      amount_list , grand_total_base_price, grand_total_source_base_price,\
-      grand_total_duration, grand_total_duration_france, grand_total_variated_base_price,\
-      grand_total_variated_source_base_price = my_transformation.getAggregatedAmountList(
-                      REQUEST = {'categories':  my_context_movement.getVariationCategoryList()} )
-                                                # Coramy Specific
-
-      # Create a line for the resource produced by the transformation
-      new_id = 'produced_resource'
-      produced_resource = applied_rule.get(new_id)
-      if produced_resource is None:
-        my_context_movement.portal_types.constructContent(
-              type_name = delivery_line_type,
-              container = applied_rule,
-              id = new_id,
-          ) # quantity
-        lost_quantity = 0.0
+      parent_movement = applied_rule.getParent()
+      # Get production node and production section
+      production = parent_movement.getSource()
+      production_section = parent_movement.getSourceSection()
+      # Get the current supply link used to calculate consumed resource
+      # The current supply link is calculated from the parent AppliedRule.
+      supply_chain = self.getSupplyChain(parent_movement.getParent())
+      parent_supply_link = self.getCurrentSupplyLink(parent_movement)
+      current_supply_link_list = supply_chain.\
+                     getPreviousProductionSupplyLinkList(parent_supply_link)
+      if len(current_supply_link_list) != 1:
+        # We shall no pass here.
+        # The test method returned a wrong value !
+        raise "TransformationRuleError",\
+              "Expand must not be called on %r" %\
+                  applied_rule.getRelativeUrl()
       else:
-        lost_quantity = produced_resource.getLostQuantity()
-
-      produced_resource = applied_rule[new_id]
-      produced_resource._edit(
-        start_date = my_context_movement.getStartDate(),
-        stop_date = my_context_movement.getStartDate(),
-        resource = my_context_movement.getResource(),
-        quantity = my_context_movement.getQuantity() + lost_quantity,
-        source_list = (),
-        source_section_list = (),
-        quantity_unit = my_context_movement.getQuantityUnit(),
-        destination_section = production_section,
-        destination = production_node,
-        deliverable = 1
-      )
-      # Mising quantity unit conversion for my_quantity !!!! XXXX
-      produced_resource.setVariationCategoryList(my_context_movement.getVariationCategoryList())
-
-      # Add lines
-      line_number = 0
-      acceptable_id_list = ['produced_resource']
-      # getRootAppliedRules is not defined
-      #production_order = self.getRootAppliedRule().getCausalityValue() # get the production order
-      production_order = applied_rule.getRootAppliedRule().getCausalityValue() # get the production order
-      filter_list = production_order.contentValues(filter={'portal_type': 'Amount Filter'})
-      for amount_line in amount_list:
-        # Apply each amount filter
-        for f in filter_list:
-          f.update(amount_line)
-        new_id = 'transformed_resource_%s' % line_number
-        transformed_resource = applied_rule.get(new_id)
-        if transformed_resource is None:
-          my_context_movement.portal_types.constructContent(
-              type_name = delivery_line_type,
-              container = applied_rule,
-              id = new_id,
-          ) # quantity
-        transformed_resource = applied_rule[new_id]
-        #LOG("amount_line", 0, str(amount_line))
-        if amount_line['quantity'] != 0.0:
-          # Only create line if it is not 0.0
-          transformed_resource._edit(
-            start_date = my_context_movement.getStartDate(),
-            stop_date = my_context_movement.getStartDate(),
-            quantity = amount_line['quantity'] * my_quantity,
-            efficiency = amount_line['efficiency'],
-            resource_value = amount_line['resource'],
-            quantity_unit = amount_line['quantity_unit'],
-            source = production_node,
-            source_section = production_section,
-            destination_list = (),
-            deliverable = 1
-          )
-          LOG('TransformationRule.expand transformed_resource.getPhysicalPath()',0,transformed_resource.getPhysicalPath())
-          LOG('TransformationRule.expand transformed_resource.getTargetSource()',0,transformed_resource.getTargetSource())
-          LOG('TransformationRule.expand transformed_resource.getSource()',0,transformed_resource.getSource())
-          #LOG('RESOURCE', 0, str(amount_line['resource'].getRelativeUrl()))
-          #LOG('VC List', 0, str(amount_line['variation_category_list']))
-          #LOG('Quantity', 0, str(amount_line['quantity']))
-          #LOG('Co Quantity', 0, str(amount_line['converted_quantity']))
-          variation_category_list = amount_line['variation_category_list']
-          # Verify each category
-          category_list = []
-          for category in variation_category_list:
-            value = self.portal_simulation.resolveCategory(category)
-            if value is not None:
-              category_list += [category]
-          transformed_resource.setVariationCategoryList(category_list)
-          acceptable_id_list += [new_id]
-          LOG('TransformationRule.expand transformed_resource.getPhysicalPath()',0,transformed_resource.getPhysicalPath())
-          LOG('TransformationRule.expand transformed_resource.getTargetSource()',0,transformed_resource.getTargetSource())
-          LOG('TransformationRule.expand transformed_resource.getSource()',0,transformed_resource.getSource())
-          LOG('TransformationRule.expand transformed_resource.showDict()',0,transformed_resource.showDict())
-        line_number += 1
+        current_supply_link = current_supply_link_list[0]
+        # Generate produced movement
+        movement_dict = self._expandProducedResource(applied_rule, 
+                                                     production,
+                                                     production_section,
+                                                     current_supply_link)
+        # Generate consumed movement
+        consumed_mvt_dict = self._expandConsumedResource(applied_rule, 
+                                                         production,
+                                                         production_section,
+                                                         current_supply_link)
+        movement_dict.update(consumed_mvt_dict)
+        # Finally, build movement
+        self._buildMovementList(applied_rule, movement_dict)
+      # Expand each movement created
+      Rule.expand(self, applied_rule, **kw)
 
-      # Remove each movement not in the transformation
-      for movement in applied_rule.objectValues():
-        if movement.getId() not in acceptable_id_list:
-          movement.flushActivity(invoke=0)
-          applied_rule._delObject(movement.getId()) # XXXX Make sur this is not deleted if already in delivery
-        LOG('TransformationRule.expand movement.getPhysicalPath()',0,movement.getPhysicalPath())
-        LOG('TransformationRule.expand movement.getTargetSource()',0,movement.getTargetSource())
-        LOG('TransformationRule.expand movement.getSource()',0,movement.getSource())
-        LOG('TransformationRule.expand movement.getTargetSource',0,movement.getTargetSource)
-        LOG('TransformationRule.expand movement.showDict()',0,movement.showDict())
+    def _expandProducedResource(self, applied_rule, production,
+                                production_section, current_supply_link):
+      """
+        Produced resource.
+        Create a movement for the resource produced by the transformation.
+        Only one produced movement can be created.
+      """
+      parent_movement = applied_rule.getParent()
+      produced_movement_dict = {
+        'pr': {
+          "resource": parent_movement.getResource(),
+          # XXX what is lost quantity ?
+          "quantity": parent_movement.getQuantity(),# + lost_quantity,
+          "quantity_unit": parent_movement.getQuantityUnit(),
+          "variation_category_list":\
+                        parent_movement.getVariationCategoryList(),
+          "source_list": (),
+          "source_section_list": (),
+          "destination": production,
+          "destination_section": production_section,
+          "deliverable": 1,
+            # XXX FIXME date not implemented
+  #         "start_date": parent_movement.getStartDate(),
+  #         "stop_date": parent_movement.getStartDate(),
+          'causality_value': current_supply_link,
+        }
+      }
+      return produced_movement_dict
 
-      # Pass to base class
-      Rule.expand(self, applied_rule, **kw)
+    def _expandConsumedResource(self, applied_rule, production,
+                                production_section, current_supply_link):
+      """
+        Consumed resource.
+        Create a movement for each resource consumed by the transformation,
+        and for the previous variation of the produced resource.
+      """
+      # Calculate all consumed resource
+      # Store each value in a dictionnary before created them.
+      # { movement_id: {property_name: property_value,} ,}
+      consumed_movement_dict = {}
+      parent_movement = applied_rule.getParent()
+      supply_chain = self.getSupplyChain(parent_movement.getParent())
+      # Consumed previous variation
+      previous_variation_dict = self._expandConsumedPreviousVariation(
+                                                        applied_rule, 
+                                                        production, 
+                                                        production_section,
+                                                        supply_chain,
+                                                        current_supply_link)
+      consumed_movement_dict.update(previous_variation_dict)
+      # Consumed raw materials
+      raw_material_dict = self._expandConsumedRawMaterials(
+                                                        applied_rule, 
+                                                        production, 
+                                                        production_section,
+                                                        supply_chain,
+                                                        current_supply_link)
+      consumed_movement_dict.update(raw_material_dict)
+      return consumed_movement_dict
+
+    def _expandConsumedPreviousVariation(self, applied_rule, production,
+                                         production_section, supply_chain,
+                                         current_supply_link):
+      """
+        Create a movement for the previous variation of the produced resource.
+      """
+      consumed_movement_dict = {}
+      parent_movement = applied_rule.getParent()
+      # First, calculate the previous variation of the previous resource
+      previous_ind_phase_list = supply_chain.\
+          getPreviousProductionIndustrialPhaseList(current_supply_link)
+      for ind_phase_value in previous_ind_phase_list:
+        ind_phase = ind_phase_value.getLogicalPath()
+        consumed_mvt_id = "%s_%s" % ("mr", ind_phase_value.getId())
+        consumed_movement_dict[consumed_mvt_id] = {
+          # XXX FIXME: Not yet implemented
+#           start_date = parent_movement.getStartDate(),
+#           stop_date = parent_movement.getStartDate(),
+          "resource": parent_movement.getResource(),
+          # XXX Is the quantity value correct ?
+          "quantity": parent_movement.getQuantity(),
+          "quantity_unit": parent_movement.getQuantityUnit(),
+          "destination_list": (),
+          "destination_section_list": (),
+          "source": production,
+          "source_section": production_section,
+          "deliverable": 1,
+          "variation_category_list": \
+                        parent_movement.getVariationCategoryList(),
+          'causality_value': current_supply_link,
+          "industrial_phase": ind_phase}
+      return consumed_movement_dict
+
+    def _expandConsumedRawMaterials(self, applied_rule, production,
+                                    production_section, supply_chain,
+                                    current_supply_link):
+      """
+        Create a movement for each resource consumed by the transformation,
+      """
+      parent_movement = applied_rule.getParent()
+      # Calculate the context for getAggregatedAmountList
+      base_category_list = parent_movement.getVariationBaseCategoryList()
+      if "industrial_phase" in base_category_list:
+        # We do not want to get the industrial phase variation
+        base_category_list.remove("industrial_phase")
+      category_list = parent_movement.getVariationCategoryList(
+                                  base_category_list=base_category_list)
+      # Get the transformation to use
+      production_order = applied_rule.getRootAppliedRule().\
+                                                     getCausalityValue()
+      transformation = production_order.getSpecialiseValue(
+                         portal_type=self.getPortalTransformationTypeList())
+      # Generate the fake context 
+      tmp_context = parent_movement.asContext(
+                   context=parent_movement, 
+                   REQUEST={'categories':category_list})
+      # Calculate the industrial phase list
+      previous_ind_phase_list = supply_chain.\
+          getPreviousPackingListIndustrialPhaseList(current_supply_link)
+      ind_phase_id_list = [x.getId() for x in previous_ind_phase_list]
+      # Call getAggregatedAmountList
+      amount_list = transformation.getAggregatedAmountList(
+                   tmp_context,
+                   ind_phase_id_list=ind_phase_id_list)
+      # Add entries in the consumed_movement_dict
+      consumed_movement_dict = {}
+      for amount in amount_list:
+        consumed_mvt_id = "%s_%s" % ("cr", amount.getId())
+        consumed_movement_dict[consumed_mvt_id] = {
+          # XXX FIXME date not implemented
+#             "start_date": movement.getStartDate(),
+#             "stop_date": movement.getStartDate(),
+          "resource": amount.getResource(),
+          "variation_category_list":\
+                        amount.getVariationCategoryList(),
+          "quantity": amount.getQuantity() * parent_movement.getQuantity(),
+          "quantity_unit": amount.getQuantityUnit(),
+          "destination_list": (),
+          "destination_section_list": (),
+          "source": production,
+          "source_section": production_section,
+          "deliverable": 1,
+          'causality_value': current_supply_link,
+        }
+      return consumed_movement_dict
 
     security.declareProtected(Permissions.ModifyPortalContent, 'solve')
     def solve(self, applied_rule, solution_list):
@@ -361,3 +329,8 @@ An ERP5 Rule..."""
     # Deliverability / orderability
     def isDeliverable(self, m):
       return 1
+    def isOrderable(self, m):
+      return 0
+
+from Products.ERP5Type.Utils import monkeyPatch
+monkeyPatch(TransformationSourcingRuleMixin, TransformationRule)
diff --git a/product/ERP5/Document/TransformationSourcingRule.py b/product/ERP5/Document/TransformationSourcingRule.py
index 24ad3c1c9b..37fba65bd2 100755
--- a/product/ERP5/Document/TransformationSourcingRule.py
+++ b/product/ERP5/Document/TransformationSourcingRule.py
@@ -1,7 +1,8 @@
 ##############################################################################
 #
-# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
 #                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Romain Courteaud <romain@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,6 +27,8 @@
 #
 ##############################################################################
 
+import ExtensionClass
+
 from AccessControl import ClassSecurityInfo
 from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
 from Products.CMFCore.utils import getToolByName
@@ -35,110 +38,107 @@ from Products.ERP5.Document.Rule import Rule
 
 from zLOG import LOG
 
+class TransformationSourcingRuleMixin(ExtensionClass.Base):
+  """
+    Mixin class used by TransformationSourcingRule and TransformationRule
+  """
+  # Declarative security
+  security = ClassSecurityInfo()
+
+  security.declareProtected(Permissions.View,
+                            'getSupplyChain')
+  def getSupplyChain(self, applied_rule):
+    """
+      Get the SupplyChain.
+    """
+    # Get the SupplyChain to use
+    supply_chain_portal_type = "Supply Chain"
+    order = applied_rule.getRootAppliedRule().getCausalityValue()
+    supply_chain = order.getSpecialiseValue(
+                               portal_type=supply_chain_portal_type)
+    if supply_chain is None:
+      raise "ProductionOrderError",\
+            "No SupplyChain defined on %s" % str(order)
+    else:
+      return supply_chain
+
+  def getCurrentSupplyLink(self, movement):
+    """
+      Get the current SupplyLink
+    """
+    # Get the current supply link
+    supply_link_portal_type = "Supply Link"
+    current_supply_link = movement.getCausalityValue(
+                                  portal_type=supply_link_portal_type)
+    return current_supply_link
+
+  security.declareProtected(Permissions.ModifyPortalContent, 
+                            '_buildMovementList')
+  def _buildMovementList(self, applied_rule, movement_dict):
+    """
+      For each movement in the dictionnary, test if the movement already
+      exists.
+      If not, create it.
+      Then, update the movement attributes.
+    """
+    for movement_id in movement_dict.keys():
+      movement = applied_rule.get(movement_id)
+      # Create the movement if it does not exist
+      if movement is None:
+        movement = applied_rule.newContent(
+                        portal_type=self.simulation_movement_portal_type,
+                        id=movement_id
+        )
+      # Update movement properties
+      movement.edit(**(movement_dict[movement_id]))
+
 class TransformationSourcingRule(Rule):
     """
       Transformation Sourcing Rule object make sure
       items required in a Transformation are sourced
     """
-
     # CMF Type Definition
     meta_type = 'ERP5 Transformation Sourcing Rule'
     portal_type = 'Transformation Sourcing Rule'
-    add_permission = Permissions.AddPortalContent
-    isPortalContent = 1
-    isRADContent = 1
-
     # Declarative security
     security = ClassSecurityInfo()
     security.declareObjectProtected(Permissions.View)
-
     # Default Properties
     property_sheets = ( PropertySheet.Base
                       , PropertySheet.XMLObject
                       , PropertySheet.CategoryCore
                       , PropertySheet.DublinCore
                       )
-
-    # CMF Factory Type Information
-    factory_type_information = \
-      {    'id'             : portal_type
-         , 'meta_type'      : meta_type
-         , 'description'    : """\
-An ERP5 Rule..."""
-         , 'icon'           : 'rule_icon.gif'
-         , 'product'        : 'ERP5'
-         , 'factory'        : 'addTransformationSourcingRule'
-         , 'immediate_view' : 'rule_view'
-         , 'allow_discussion'     : 1
-         , 'allowed_content_types': ()
-         , 'filter_content_types' : 1
-         , 'global_allow'   : 1
-         , 'actions'        :
-        ( { 'id'            : 'view'
-          , 'name'          : 'View'
-          , 'category'      : 'object_view'
-          , 'action'        : 'rule_view'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'list'
-          , 'name'          : 'Object Contents'
-          , 'category'      : 'object_action'
-          , 'action'        : 'folder_contents'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'print'
-          , 'name'          : 'Print'
-          , 'category'      : 'object_print'
-          , 'action'        : 'rule_print'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'metadata'
-          , 'name'          : 'Metadata'
-          , 'category'      : 'object_view'
-          , 'action'        : 'metadata_edit'
-          , 'permissions'   : (
-              Permissions.View, )
-          }
-        , { 'id'            : 'translate'
-          , 'name'          : 'Translate'
-          , 'category'      : 'object_action'
-          , 'action'        : 'translation_template_view'
-          , 'permissions'   : (
-              Permissions.TranslateContent, )
-          }
-        )
-      }
+    # Class variable 
+    simulation_movement_portal_type = "Simulation Movement"
 
     security.declareProtected(Permissions.AccessContentsInformation, 'test')
     def test(self, movement):
       """
         Tests if the rule (still) applies
       """
-      # Test if some movements are "spendings" movements
-      # ie. with  destination equal to None
-      destination = movement.getDestination()
-      if destination is not None:
-        return 0
-
-      # Is the resource sourceable (ie. tissue, composant in Coramy Case)
-      # This must become generic in the future through path XXXX
-      resource = movement.getDefaultResourceValue()
-      if resource is None:
-        return 0
-      module = resource.aq_parent
-
-      # Source components and workforce at this point
-      # This must become generic in the future through path XXXX
-      if module.id in ('tissu', 'composant'):
-        return 1
-      # We accept operations at this point
-      resource = movement.getDefaultResource()
-      if resource.find('operation/') >= 0:
-        return 1
-      return 0
+      # Test if we must transform
+      # The test should actually be based on nodes, paths
+      # and capacities, which is not possible now
+      result = 1
+      # Only apply to Order applied rule
+      root_applied_rule = movement.getRootAppliedRule()
+      root_rule = root_applied_rule.getSpecialiseValue()
+      order = root_applied_rule.getCausalityValue()
+      # Test some properties to see if we are really 
+      # in a 'production' expand.
+      if (root_rule is None) or\
+         (root_rule.getPortalType() != "Production Order Rule") or\
+         (order is None) or\
+         (movement.getResourceValue() is None) or\
+         (movement.getSourceValue() is None):
+           result = 0
+      else:
+        supply_chain = self.getSupplyChain(movement.getParent())
+        parent_supply_link = self.getCurrentSupplyLink(movement)
+        if not supply_chain.test(parent_supply_link, movement):
+          result = 0
+      return result
 
     # Simulation workflow
     def reset(self, applied_rule):
@@ -156,73 +156,50 @@ An ERP5 Rule..."""
     def expand(self, applied_rule, **kw):
       """
         Expands the current movement downward.
-
         -> new status -> expanded
-
         An applied rule can be expanded only if its parent movement
         is expanded.
       """
-      delivery_line_type = 'Simulation Movement'
-
-      # Source that movement from the next node / stock
-      my_context_movement = applied_rule.getParent()
-      LOG('TransformationSourcingRule.expand, my_context_movement.getPhysicalPath()',0,my_context_movement.getPhysicalPath())
-      LOG('TransformationSourcingRule.expand, my_context_movement.getSource()',0,my_context_movement.getSource())
-      LOG('TransformationSourcingRule.expand, my_context_movement.getTargetSource()',0,my_context_movement.getTargetSource())
-      LOG('TransformationSourcingRule.expand, my_context_movement.showDict()',0,my_context_movement.showDict())
-      LOG('TransformationSourcingRule.expand, my_context_movement.getTargetSource',0,my_context_movement.getTargetSource)
-      if my_context_movement.getSource() is not None:
-        # We should only expand movements if they have a source
-        # otherwise, it creates infinite recursion
-        # This happens for example whenever the source of a movement is acquired
-        # from an order which is deleted afterwards
-        # LOG('Sourcing', 0, str(my_context_movement.getDefaultResource()))
-        new_id = 'transformation_source'
-        transformation_source = getattr(aq_base(applied_rule), new_id, None)
-        if transformation_source is None:
-          my_context_movement.portal_types.constructContent(
-                type_name = delivery_line_type,
-                container = applied_rule,
-                id = new_id
-              )
-        transformation_source = applied_rule[new_id]
-
-        resource = my_context_movement.getResource()
-        if resource.find('operation/') >= 0:
-          # This is an operation - produce it
-          transformation_source._edit(
-                  target_quantity = my_context_movement.getTargetQuantity(),
-                  target_efficiency = my_context_movement.getTargetEfficiency(),
-                  resource = resource,
-                  target_start_date = my_context_movement.getTargetStartDate(),
-                  target_stop_date = my_context_movement.getTargetStartDate(),
-                  target_source_list = (),
-                  target_source_section_list = (),
-                  quantity_unit = my_context_movement.getQuantityUnit(),
-                  target_destination = my_context_movement.getTargetSource(),
-                  target_destination_section = my_context_movement.getTargetSourceSection(),
-                  deliverable = 0   # We do not need to source explicitely operations
-              )
-          transformation_source.setVariationCategoryList(
-                    my_context_movement.getVariationCategoryList())
-        else:
-          # This is a component - source from Stock
-          transformation_source._edit(
-                  target_quantity = my_context_movement.getTargetQuantity(),
-                  target_efficiency = my_context_movement.getTargetEfficiency(),
-                  resource = resource,
-                  target_start_date = my_context_movement.getTargetStartDate(),
-                  target_stop_date = my_context_movement.getTargetStartDate(),
-                  target_source = 'site/Stock_MP/Gravelines',
-                  target_source_section = 'group/Coramy',
-                  quantity_unit = my_context_movement.getQuantityUnit(),
-                  target_destination = my_context_movement.getTargetSource(),
-                  target_destination_section = my_context_movement.getTargetSourceSection(),
-                  deliverable = 1,
-              )
-          transformation_source.setVariationCategoryList(
-                    my_context_movement.getVariationCategoryList())
-
+      parent_movement = applied_rule.getParent()
+      # Calculate the previous supply link
+      supply_chain = self.getSupplyChain(parent_movement.getParent())
+      parent_supply_link = self.getCurrentSupplyLink(parent_movement)
+      previous_supply_link_list = supply_chain.\
+                     getPreviousPackingListSupplyLinkList(
+                                                    parent_supply_link,
+                                                    movement=parent_movement)
+      if len(previous_supply_link_list) == 0:
+        raise "TransformationSourcingRuleError",\
+              "Expand must not be called on %r" %\
+                  applied_rule.getRelativeUrl()
+      else:
+        movement_dict = {}
+        for previous_supply_link in previous_supply_link_list:
+          # Calculate the source
+          source_value = None
+          source_node = previous_supply_link.getSourceValue()
+          if source_node is not None:
+            source_value = source_node.getDestinationValue()
+          # Generate the dict
+          movement_dict.update({
+            "ts": {
+              'source_value': source_value,
+              'destination_value': parent_movement.getSourceValue(),
+              'resource_value': parent_movement.getResourceValue(),
+              'variation_category_list': parent_movement.\
+                                            getVariationCategoryList(),
+              'quantity': parent_movement.getQuantity(),
+              'quantity_unit': parent_movement.getQuantityUnit(),
+              # XXX FIXME not implemented
+    #           'start_date': parent_movement.getStartDate(),
+    #           'stop_date': parent_movement.getStopDate(),
+              'deliverable': 1,
+              # Save the value of the current supply link
+              'causality_value': previous_supply_link,
+            }
+          })
+        # Build the movement
+        self._buildMovementList(applied_rule, movement_dict)
       # Create one submovement which sources the transformation
       Rule.expand(self, applied_rule, **kw)
 
@@ -276,3 +253,9 @@ An ERP5 Rule..."""
         return 0
       else:
         return 1
+
+    def isOrderable(self, m):
+      return 0
+
+from Products.ERP5Type.Utils import monkeyPatch
+monkeyPatch(TransformationSourcingRuleMixin, TransformationSourcingRule)
-- 
2.30.9