From 662d87983152fd4fb4c1ffd0268a35f7ff3a3c2e Mon Sep 17 00:00:00 2001
From: Yusuke Muraoka <yusuke@nexedi.com>
Date: Mon, 8 Jun 2009 10:02:09 +0000
Subject: [PATCH] - changed the TransformationSourcingRule to refer a business
 process   instead of a supply chain - added a test for
 TransformationSourcingRule

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@27405 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/TransformationRule.py   | 110 +++--
 .../Document/TransformationSourcingRule.py    | 304 ++++----------
 product/ERP5/tests/testMRP.py                 | 396 ++++++++++++++----
 3 files changed, 464 insertions(+), 346 deletions(-)

diff --git a/product/ERP5/Document/TransformationRule.py b/product/ERP5/Document/TransformationRule.py
index 8b06d782b3..c3cba351c3 100644
--- a/product/ERP5/Document/TransformationRule.py
+++ b/product/ERP5/Document/TransformationRule.py
@@ -38,17 +38,12 @@ from Products.ERP5.Document.Rule import Rule
 from Products.ERP5.Document.SimulationMovement import SimulationMovement
 from Products.ERP5Type.Errors import TransformationRuleError
 
-class TransformationMovementFactory:
-  def __init__(self):
-    self.default = None # base information to use for making movements
-    self.produced_list = list()
-    self.consumed_list = list()
-
-  def requestProduced(self, **produced):
-    self.produced_list.append(produced)
-
-  def requestConsumed(self, **consumed):
-    self.consumed_list.append(consumed)
+class MovementFactory:
+  def getRequestList(self):
+    """
+    return the list of a request which to be used to apply movements
+    """
+    raise NotImplementedError, 'Must be implemented'
 
   def _getCausalityList(self, causality=None, causality_value=None,
                         causality_list=None, causality_value_list=None,
@@ -63,17 +58,6 @@ class TransformationMovementFactory:
       return [causality_value.getRelativeUrl()
               for causality_value in causality_value_list]
 
-  def getRequestList(self):
-    _list = []
-    for (request_list, sign) in ((self.produced_list, -1),
-                            (self.consumed_list, 1)):
-      for request in request_list:
-        d = self.default.copy()
-        d.update(request)
-        d['quantity'] *= sign
-        _list.append(d)
-    return _list
-
   def makeMovements(self, applied_rule):
     """
     make movements under the applied_rule by requests
@@ -84,10 +68,6 @@ class TransformationMovementFactory:
       key = tuple(sorted(movement.getCausalityList()))
       movement_dict[key] = movement
 
-    """
-    produced quantity should be represented by minus quantity on movement.
-    because plus quantity is consumed.
-    """ 
     for request in self.getRequestList():
       # get movement by causality
       key = tuple(sorted(self._getCausalityList(**request)))
@@ -121,6 +101,37 @@ class TransformationMovementFactory:
       diff_movement.edit(**request)
 
 
+class TransformationMovementFactory(MovementFactory):
+  def __init__(self):
+    self.product = None # base information to use for making movements
+    self.produced_list = list()
+    self.consumed_list = list()
+
+  def requestProduced(self, **produced):
+    self.produced_list.append(produced)
+
+  def requestConsumed(self, **consumed):
+    self.consumed_list.append(consumed)
+
+  def getRequestList(self):
+    """
+    return the list of a request which to be used to apply movements
+    """
+    _list = []
+    """
+    produced quantity should be represented by minus quantity on movement.
+    because plus quantity is consumed.
+    """ 
+    for (request_list, sign) in ((self.produced_list, -1),
+                                 (self.consumed_list, 1)):
+      for request in request_list:
+        d = self.product.copy()
+        d.update(request)
+        d['quantity'] *= sign
+        _list.append(d)
+    return _list
+
+
 class TransformationRuleMixin(Base):
   security = ClassSecurityInfo()
 
@@ -266,7 +277,7 @@ class TransformationRule(TransformationRuleMixin, Rule):
     head_production_path_list = self.getHeadProductionPathList(transformation,
                                                                business_process)
     factory = self.getFactory()
-    factory.default = dict(
+    factory.product = dict(
       resource=transformation.getResource(),
       quantity=parent_movement.getNetQuantity(),
       quantity_unit=parent_movement.getQuantityUnit(),
@@ -299,6 +310,21 @@ class TransformationRule(TransformationRuleMixin, Rule):
               % (phase, business_process)
 
       for path in phase_path_list:
+        # source, source_section
+        source_section = path.getSourceSection() # only support a static access 
+        source_method_id = path.getSourceMethodId()
+        if source_method_id is None:
+          source = path.getSource()
+        else:
+          source = getattr(path, source_method_id)()
+        # destination, destination_section
+        destination_section = path.getDestinationSection() # only support a static access 
+        destination_method_id = path.getDestinationMethodId()
+        if destination_method_id is None:
+          destination = path.getDestination()
+        else:
+          destination = getattr(path, destination_method_id)()
+
         start_date = path.getExpectedStartDate(explanation)
         stop_date = path.getExpectedStopDate(explanation)
         predecessor_remaining_phase_list = path.getPredecessorValue()\
@@ -307,7 +333,6 @@ class TransformationRule(TransformationRuleMixin, Rule):
         successor_remaining_phase_list = path.getSuccessorValue()\
           .getRemainingTradePhaseList(explanation,
                                       trade_phase_list=trade_phase_list)
-        destination = path.getDestination()
 
         # checking which is not last path of transformation
         if len(successor_remaining_phase_list) != 0:
@@ -317,9 +342,10 @@ class TransformationRule(TransformationRuleMixin, Rule):
             start_date=start_date,
             stop_date=stop_date,
             # when last path of transformation, path.getQuantity() will be return 1.
-            quantity=factory.default['quantity'] * path.getQuantity(),
+            quantity=factory.product['quantity'] * path.getQuantity(),
+            source_section=source_section,
+            destination_section=destination_section,
             destination=destination,
-            #destination_section=???,
             trade_phase_value_list=successor_remaining_phase_list)
         else:
           # for making movement of last product of the transformation
@@ -333,12 +359,19 @@ class TransformationRule(TransformationRuleMixin, Rule):
           # trade phase of product is must be empty []
           if last_prop_dict.get('trade_phase_value_list', None) is None:
             last_prop_dict['trade_phase_value_list'] = successor_remaining_phase_list
+          if last_prop_dict.get('source_section', None) is None:
+            last_prop_dict['source_section'] = source_section
+          # for the source, it is not need, because the produced.
+          if last_prop_dict.get('destination_section', None) is None:
+            last_prop_dict['destination_section'] = destination_section
           if last_prop_dict.get('destination', None) is None:
             last_prop_dict['destination'] = destination
 
           if last_prop_dict['start_date'] != start_date or\
              last_prop_dict['stop_date'] != stop_date or\
              last_prop_dict['trade_phase_value_list'] != successor_remaining_phase_list or\
+             last_prop_dict['source_section'] != source_section or\
+             last_prop_dict['destination_section'] != destination_section or\
              last_prop_dict['destination'] != destination:
             raise TransformationRuleError,\
               """Returned property is different on Transformation %r and Business Process %r"""\
@@ -350,9 +383,10 @@ class TransformationRule(TransformationRuleMixin, Rule):
             causality_value=path,
             start_date=start_date,
             stop_date=stop_date,
-            quantity=factory.default['quantity'] * path.getQuantity(),
-            source=path.getSource(),
-            #source_section=???,
+            quantity=factory.product['quantity'] * path.getQuantity(),
+            source_section=source_section,
+            destination_section=destination_section,
+            source=source,
             trade_phase_value_list=predecessor_remaining_phase_list)
 
         # consumed movement
@@ -362,11 +396,12 @@ class TransformationRule(TransformationRuleMixin, Rule):
             start_date=start_date,
             stop_date=stop_date,
             resource=amount.getResource(),
-            quantity=factory.default['quantity'] * amount.getQuantity()\
+            quantity=factory.product['quantity'] * amount.getQuantity()\
               / amount.getEfficiency() * path.getQuantity(),
             quantity_unit=amount.getQuantityUnit(),
-            source=path.getSource(),
-            #source_section=???,
+            source_section=source_section,
+            destination_section=destination_section,
+            source=source,
             trade_phase=path.getTradePhase())
 
     """
@@ -398,8 +433,7 @@ which last_phase_path_list is empty.""" % (transformation, business_process)
     factory.requestProduced(
       causality_value_list=last_phase_path_list,
       # when last path of transformation, path.getQuantity() will be return 1.
-      quantity=factory.default['quantity'] * path.getQuantity(),
-      #destination_section=???,
+      quantity=factory.product['quantity'] * path.getQuantity(),
       **last_prop_dict)
 
     factory.makeMovements(applied_rule)
diff --git a/product/ERP5/Document/TransformationSourcingRule.py b/product/ERP5/Document/TransformationSourcingRule.py
index b8c3f81570..9eac3e31cb 100644
--- a/product/ERP5/Document/TransformationSourcingRule.py
+++ b/product/ERP5/Document/TransformationSourcingRule.py
@@ -27,240 +27,114 @@
 #
 ##############################################################################
 
-import ExtensionClass
-
 from AccessControl import ClassSecurityInfo
 from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
 from Products.CMFCore.utils import getToolByName
 
 from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces
 from Products.ERP5.Document.Rule import Rule
+from Products.ERP5.Document.TransformationRule import MovementFactory, TransformationRuleMixin
 
 from zLOG import LOG
 
-class ProductionOrderError(Exception): pass
 class TransformationSourcingRuleError(Exception): pass
 
-class TransformationSourcingRuleMixin(ExtensionClass.Base):
+class SourcingMovementFactory(MovementFactory):
+  def __init__(self):
+    self.request_list = list()
+
+  def requestSourcing(self, **sourcing):
+    self.request_list.append(sourcing)
+
+  def getRequestList(self):
+    return self.request_list
+
+class TransformationSourcingRule(TransformationRuleMixin, Rule):
   """
-    Mixin class used by TransformationSourcingRule and TransformationRule
+  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'
   # 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,activate_kw=None,**kw):
-    """
-      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,
-                        activate_kw=activate_kw
-        )
-      # We shouldn't modify frozen movements
-      elif movement.isFrozen():
-        # FIXME: this is not perfect, instead of just skipping this one, we
-        # should generate a compensation movement
-        continue
-      # Update movement properties
-      movement.edit(activate_kw=activate_kw, **(movement_dict[movement_id]))
-
-  security.declareProtected(Permissions.View, 'getTransformation')
-  def getTransformation(self, movement):
-    """
-    Get transformation related to used by the applied rule.
-    """
-    production_order_movement = movement.getRootSimulationMovement().\
-                                                   getOrderValue()
-    # XXX Acquisition can be use instead
-    parent_uid = production_order_movement.getParentUid()
-    explanation_uid = production_order_movement.getExplanationUid()
-    if parent_uid == explanation_uid:
-      production_order_line = production_order_movement
-    else:
-      production_order_line = production_order_movement.getParentValue()
-    script = production_order_line._getTypeBasedMethod('_getTransformation') 
-    if script is not None:
-      transformation = script()
-    else:
-      line_transformation = production_order_line.objectValues(
-                portal_type=self.getPortalTransformationTypeList())
-      if len(line_transformation)==1:
-        transformation = line_transformation[0]
-      else:
-        transformation = production_order_line.getSpecialiseValue(
-                           portal_type=self.getPortalTransformationTypeList())
-    return transformation
-
-class TransformationSourcingRule(TransformationSourcingRuleMixin, 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'
-    # Declarative security
-    security = ClassSecurityInfo()
-    security.declareObjectProtected(Permissions.AccessContentsInformation)
-    __implements__ = ( interfaces.IPredicate,
-                       interfaces.IRule )
-    # Default Properties
-    property_sheets = ( PropertySheet.Base
+  security.declareObjectProtected(Permissions.AccessContentsInformation)
+  __implements__ = ( interfaces.IPredicate,
+                     interfaces.IRule )
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
                       , PropertySheet.XMLObject
                       , PropertySheet.CategoryCore
                       , PropertySheet.DublinCore
                       , PropertySheet.Task
                       )
-    # Class variable 
-    simulation_movement_portal_type = "Simulation Movement"
-
-    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
-    def expand(self, applied_rule, activate_kw=None,**kw):
-      """
-        Expands the current movement downward.
-        -> new status -> expanded
-        An applied rule can be expanded only if its parent movement
-        is expanded.
-      """
-      parent_movement = applied_rule.getParentValue()
-      # Calculate the previous supply link
-      supply_chain = self.getSupplyChain(parent_movement.getParentValue())
-      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()
-          source_section_value = previous_supply_link.getSourceSectionValue()
-          # Generate the dict
-          stop_date = parent_movement.getStartDate()
-          movement_dict.update({
-            "ts": {
-              'source_value': source_value,
-              'source_section_value': source_section_value,
-              'destination_value': parent_movement.getSourceValue(),
-              'destination_section_value': \
-                  parent_movement.getSourceSectionValue(),
-              'resource_value': parent_movement.getResourceValue(),
-              'variation_category_list': parent_movement.\
-                                            getVariationCategoryList(),
-              "variation_property_dict": \
-                            parent_movement.getVariationPropertyDict(),
-              'quantity': parent_movement.getNetQuantity(), # getNetQuantity to support efficency from transformation
-              'price': parent_movement.getPrice(),
-              'quantity_unit': parent_movement.getQuantityUnit(),
-              'start_date': previous_supply_link.calculateStartDate(stop_date),
-              'stop_date': stop_date,
-              'deliverable': 1,
-              # Save the value of the current supply link
-              'causality_value': previous_supply_link,
-            }
-          })
-        # Build the movement
-        self._buildMovementList(applied_rule, movement_dict,
-                                activate_kw=activate_kw)
-      # Create one submovement which sources the transformation
-      Rule.expand(self, applied_rule, activate_kw=activate_kw, **kw)
-
-    security.declareProtected(Permissions.ModifyPortalContent, 'solve')
-    def solve(self, applied_rule, solution_list):
-      """
-        Solve inconsistency according to a certain number of solutions
-        templates. This updates the
-
-        -> new status -> solved
-
-        This applies a solution to an applied rule. Once
-        the solution is applied, the parent movement is checked.
-        If it does not diverge, the rule is reexpanded. If not,
-        diverge is called on the parent movement.
-      """
-
-    security.declareProtected(Permissions.ModifyPortalContent, 'diverge')
-    def diverge(self, applied_rule):
-      """
-        -> new status -> diverged
+  def getFactory(self):
+    return SourcingMovementFactory()
 
-        This basically sets the rule to "diverged"
-        and blocks expansion process
-      """
-
-#    # Solvers
-#    security.declareProtected(Permissions.View, 'isDivergent')
-#    def isDivergent(self, applied_rule):
-#      """
-#        Returns 1 if divergent rule
-#      """
-#
-#    security.declareProtected(Permissions.View, 'getDivergenceList')
-#    def getDivergenceList(self, applied_rule):
-#      """
-#        Returns a list Divergence descriptors
-#      """
-#
-#    security.declareProtected(Permissions.View, 'getSolverList')
-#    def getSolverList(self, applied_rule):
-#      """
-#        Returns a list Divergence solvers
-#      """
-
-    def isDeliverable(self, m):
-      resource = m.getResource()
-      if m.getResource() is None:
-        return 0
-      if resource.find('operation/') >= 0:
-        return 0
-      else:
-        return 1
-
-    def isOrderable(self, m):
-      return 0
+  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.
+    """
+    parent_movement = applied_rule.getParentValue()
+    explanation = self.getExplanation(movement=parent_movement)
+    state = parent_movement.getCausalityValue().getPredecessorValue()
+    path_list = state.getSuccessorRelatedValueList()
+
+    if len(path_list) == 0:
+      raise TransformationSourcingRuleError,\
+            "Not found deliverable business path"
+    if len(path_list) > 1:
+      raise TransformationSourcingRuleError,\
+            "Found 2 or more deliverable business path"
+
+    path = path_list[0]
+
+    # source, source_section
+    source_section = path.getSourceSection() # only support a static access 
+    source_method_id = path.getSourceMethodId()
+    if source_method_id is None:
+      source = path.getSource()
+    else:
+      source = getattr(path, source_method_id)()
+    # destination, destination_section
+    destination_section = path.getDestinationSection() # only support a static access 
+    destination_method_id = path.getDestinationMethodId()
+    if destination_method_id is None:
+      destination = path.getDestination()
+    else:
+      destination = getattr(path, destination_method_id)()
+
+    start_date = path.getExpectedStartDate(explanation)
+    stop_date = path.getExpectedStopDate(explanation)
+
+    quantity = parent_movement.getNetQuantity() * path.getQuantity()
+    price = parent_movement.getPrice()
+    if price is not None:
+      price *= path.getQuantity()
+
+    factory = self.getFactory()
+    factory.requestSourcing(
+      causality_value=path,
+      source=source,
+      source_section=source_section,
+      destination=destination,
+      destination_section=destination_section,
+      resource=parent_movement.getResource(),
+      variation_category_list=parent_movement.getVariationCategoryList(),
+      variation_property_dict=parent_movement.getVariationPropertyDict(),
+      quantity=quantity,
+      price=price,
+      quantity_unit=parent_movement.getQuantityUnit(),
+      start_date=start_date,
+      stop_date=stop_date,
+      deliverable=1,
+      )
+
+    factory.makeMovements(applied_rule)
+    Rule.expand(self, applied_rule, **kw)
 
diff --git a/product/ERP5/tests/testMRP.py b/product/ERP5/tests/testMRP.py
index 872a4d4256..d111730d87 100644
--- a/product/ERP5/tests/testMRP.py
+++ b/product/ERP5/tests/testMRP.py
@@ -37,14 +37,15 @@ from Products.ERP5Type.tests.Sequence import SequenceList
 from Products.CMFCore.utils import getToolByName
 from Products.ERP5Type.tests.utils import reindex
 
-from Products.ERP5.Document.TransformationRule import TransformationRule
-
 from Products.ERP5.tests.testBPMCore import TestBPMMixin
 
 class TestMRPMixin(TestBPMMixin):
   transformation_portal_type = 'Transformation'
   transformed_resource_portal_type = 'Transformation Transformed Resource'
   product_portal_type = 'Product'
+  organisation_portal_type = 'Organisation'
+  order_portal_type = 'Production Order'
+  order_line_portal_type = 'Production Order Line'
 
   def setUpOnce(self):
     self.portal = self.getPortalObject()
@@ -57,17 +58,38 @@ class TestMRPMixin(TestBPMMixin):
     for rule in rule_tool.contentValues(
       portal_type=rule_tool.getPortalRuleTypeList()):
       rule.invalidate()
-    
-  def createTransformation(self):
+
+  def _createDocument(self, portal_type, **kw):
     module = self.portal.getDefaultModule(
-        portal_type=self.transformation_portal_type)
-    return module.newContent(portal_type=self.transformation_portal_type)
+        portal_type=portal_type)
+    return self._createObject(module, portal_type, **kw)
 
-  def createTransformedResource(self, transformation=None):
-    if transformation is None:
-      transformation = self.createTransformation()
-    return transformation.newContent(
-      portal_type=self.transformed_resource_portal_type)
+  def _createObject(self, parent, portal_type, id=None, **kw):
+    o = None
+    if id is not None:
+      o = parent.get(str(id), None)
+    if o is None:
+      o = parent.newContent(portal_type=portal_type)
+    o.edit(**kw)
+    return o
+
+  def createTransformation(self, **kw):
+    return self._createDocument(self.transformation_portal_type, **kw)
+
+  def createProduct(self, **kw):
+    return self._createDocument(self.product_portal_type, **kw)
+
+  def createOrganisation(self, **kw):
+    return self._createDocument(self.organisation_portal_type, **kw)
+
+  def createOrder(self, **kw):
+    return self._createDocument(self.order_portal_type, **kw)
+
+  def createOrderLine(self, order, **kw):
+    return self._createObject(order, self.order_line_portal_type, **kw)
+
+  def createTransformedResource(self, transformation, **kw):
+    return self._createObject(transformation, self.transformed_resource_portal_type, **kw)
 
   @reindex
   def createCategories(self):
@@ -78,39 +100,56 @@ class TestMRPMixin(TestBPMMixin):
     self.createCategoriesInCategory(category_tool.trade_phase.mrp,
         ['p' + str(i) for i in range(5)]) # phase0 ~ 4
 
-  def createProduct(self):
-    module = self.portal.getDefaultModule(
-      portal_type=self.product_portal_type)
-    return module.newContent(portal_type=self.product_portal_type)
+  @reindex
+  def createDefaultOrder(self, transformation=None, business_process=None):
+    if transformation is None:
+      transformation = self.createDefaultTransformation()
+    if business_process is None:
+      business_process = self.createSimpleBusinessProcess()
+
+    base_date = DateTime()
 
+    order = self.createOrder(specialise_value=business_process,
+                             start_date=base_date,
+                             stop_date=base_date+3)
+    order_line = self.createOrderLine(order,
+                                      quantity=10,
+                                      resource=transformation.getResource(),
+                                      specialise_value=transformation)
+    # XXX in some case, specialise_value is not related to order_line by edit,
+    #     but by setSpecialise() is ok, Why?
+    order_line.setSpecialiseValue(transformation)
+    return order
+    
   @reindex
   def createDefaultTransformation(self):
-    resource1 = self.createProduct()
-    resource2 = self.createProduct()
-    resource3 = self.createProduct()
-    resource4 = self.createProduct()
-    resource5 = self.createProduct()
-    transformation = self.createTransformation()
-    amount1 = self.createTransformedResource(transformation=transformation)
-    amount2 = self.createTransformedResource(transformation=transformation)
-    amount3 = self.createTransformedResource(transformation=transformation)
-    amount4 = self.createTransformedResource(transformation=transformation)
-
-    resource1.edit(title='product', quantity_unit_list=['weight/kg'])
-    resource2.edit(title='triangle', quantity_unit_list=['weight/kg'])
-    resource3.edit(title='box', quantity_unit_list=['weight/kg'])
-    resource4.edit(title='circle', quantity_unit_list=['weight/kg'])
-    resource5.edit(title='banana', quantity_unit_list=['weight/kg'])
-
-    transformation.edit(resource_value=resource1)
-    amount1.edit(resource_value=resource2, quantity=3,
-                 quantity_unit_list=['weight/kg'], trade_phase='mrp/p2')
-    amount2.edit(resource_value=resource3, quantity=1,
-                 quantity_unit_list=['weight/kg'], trade_phase='mrp/p2')
-    amount3.edit(resource_value=resource4, quantity=4,
-                 quantity_unit_list=['weight/kg'], trade_phase='mrp/p3')
-    amount4.edit(resource_value=resource5, quantity=1,
-                 quantity_unit_list=['weight/kg'], trade_phase='mrp/p3')
+    resource1 = self.createProduct(id='1', quantity_unit_list=['weight/kg'])
+    resource2 = self.createProduct(id='2', quantity_unit_list=['weight/kg'])
+    resource3 = self.createProduct(id='3', quantity_unit_list=['weight/kg'])
+    resource4 = self.createProduct(id='4', quantity_unit_list=['weight/kg'])
+    resource5 = self.createProduct(id='5', quantity_unit_list=['weight/kg'])
+
+    transformation = self.createTransformation(resource_value=resource5)
+    self.createTransformedResource(transformation=transformation,
+                                   resource_value=resource1,
+                                   quantity=3,
+                                   quantity_unit_list=['weight/kg'],
+                                   trade_phase='mrp/p2')
+    self.createTransformedResource(transformation=transformation,
+                                   resource_value=resource2,
+                                   quantity=1,
+                                   quantity_unit_list=['weight/kg'],
+                                   trade_phase='mrp/p2')
+    self.createTransformedResource(transformation=transformation,
+                                   resource_value=resource3,
+                                   quantity=4,
+                                   quantity_unit_list=['weight/kg'],
+                                   trade_phase='mrp/p3')
+    self.createTransformedResource(transformation=transformation,
+                                   resource_value=resource4,
+                                   quantity=1,
+                                   quantity_unit_list=['weight/kg'],
+                                   trade_phase='mrp/p3')
     return transformation
 
   @reindex
@@ -125,18 +164,34 @@ class TestMRPMixin(TestBPMMixin):
     business_state_partial = self.createBusinessState(business_process)
     business_state_done = self.createBusinessState(business_process)
 
+    # organisations
+    source_section = self.createOrganisation(title='source_section')
+    source = self.createOrganisation(title='source')
+    destination_section = self.createOrganisation(title='destination_section')
+    destination = self.createOrganisation(title='destination')
+    
     business_process.edit(referential_date='stop_date')
     business_path_p2.edit(id='p2',
                           predecessor_value=business_state_ready,
                           successor_value=business_state_partial,
                           quantity=1,
-                          trade_phase=['mrp/p2'])
+                          trade_phase=['mrp/p2'],
+                          source_section_value=source_section,
+                          source_value=source,
+                          destination_section_value=destination_section,
+                          destination_value=destination,
+                          )
     business_path_p3.edit(id='p3',
                           predecessor_value=business_state_partial,
                           successor_value=business_state_done,
                           quantity=1,
                           deliverable=1, # root explanation
-                          trade_phase=['mrp/p3'])
+                          trade_phase=['mrp/p3'],
+                          source_section_value=source_section,
+                          source_value=source,
+                          destination_section_value=destination_section,
+                          destination_value=destination,
+                          )
     return business_process
 
   @reindex
@@ -151,20 +206,51 @@ class TestMRPMixin(TestBPMMixin):
     business_state_ready = self.createBusinessState(business_process)
     business_state_partial = self.createBusinessState(business_process)
 
+    # organisations
+    source_section = self.createOrganisation(title='source_section')
+    source = self.createOrganisation(title='source')
+    destination_section = self.createOrganisation(title='destination_section')
+    destination = self.createOrganisation(title='destination')
+
     business_process.edit(referential_date='stop_date')
     business_path_p2.edit(id='p2',
                           predecessor_value=business_state_ready,
                           successor_value=business_state_partial,
                           quantity=1,
-                          trade_phase=['mrp/p2'])
+                          trade_phase=['mrp/p2'],
+                          source_section_value=source_section,
+                          source_value=source,
+                          destination_section_value=destination_section,
+                          destination_value=destination,
+                          )
     business_path_p3.edit(id='p3',
                           predecessor_value=business_state_ready,
                           successor_value=business_state_partial,
                           quantity=1,
                           deliverable=1, # root explanation
-                          trade_phase=['mrp/p3'])
+                          trade_phase=['mrp/p3'],
+                          source_section_value=source_section,
+                          source_value=source,
+                          destination_section_value=destination_section,
+                          destination_value=destination,
+                          )
     return business_process
 
+  @reindex
+  def beforeTearDown(self):
+    super(TestMRPMixin, self).beforeTearDown()
+    transaction.abort()
+    for module in (
+      self.portal.organisation_module,
+      self.portal.production_order_module, 
+      self.portal.transformation_module,
+      self.portal.business_process_module,
+      # don't remove document because reuse it for testing of id
+      # self.portal.product_module,
+      self.portal.portal_simulation,):    
+      module.manage_delObjects(list(module.objectIds()))
+    transaction.commit()
+
 class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
   """the test for implementation"""
   def test_TransformationRule_getHeadProductionPathList(self):
@@ -181,25 +267,25 @@ class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
                       set(rule.getHeadProductionPathList(transformation, business_process)))
 
   def test_TransformationRule_expand(self):
-    transformation = self.createDefaultTransformation()
+    # mock order
+    order = self.createDefaultOrder()
+    order_line = order.objectValues()[0]
 
-    """
-      Simple case
-    """
-    business_process = self.createSimpleBusinessProcess()
+    business_process = order.getSpecialiseValue()
 
-    # mock order
-    order = self.portal.production_order_module.newContent(portal_type="Production Order")
-    order_line = order.newContent(portal_type="Production Order Line")
+    # paths
+    path_p2 = '%s/p2' % business_process.getRelativeUrl()
+    path_p3 = '%s/p3' % business_process.getRelativeUrl()
 
-    base_date = DateTime()
-    order.edit(specialise_value=business_process,
-               start_date=base_date,
-               stop_date=base_date+3,
-               source_section_value=order,
-               source_value=order)
-    order_line.edit(quantity=10)
-    order_line.setSpecialiseValue(transformation) # XXX Why can not define by edit?
+    # organisations
+    path = business_process.objectValues(
+      portal_type=self.portal.getPortalBusinessPathTypeList())[0]
+    source_section = path.getSourceSection()
+    source = path.getSource()
+    destination_section = path.getDestinationSection()
+    destination = path.getDestination()
+    consumed_organisations = (source_section, source, destination_section, None)
+    produced_organisations = (source_section, None, destination_section, destination)
 
     # don't need another rules, just need TransformationRule for test
     self.invalidateRules()
@@ -213,7 +299,7 @@ class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
     applied_rule.edit(causality_value=order)
     movement.edit(order_value=order_line,
                   quantity=order_line.getQuantity(),
-                  resource=transformation.getResource())
+                  resource=order_line.getResource())
     # test mock
     applied_rule = movement.newContent(potal_type='Applied Rule')
 
@@ -222,28 +308,51 @@ class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
 
     # assertion
     expected_value_set = set([
-      (('business_process_module/1/p2',), 'product_module/1', 'mrp/p3', -10),
-      (('business_process_module/1/p2',), 'product_module/2', 'mrp/p2', 30),
-      (('business_process_module/1/p2',), 'product_module/3', 'mrp/p2', 10),
-      (('business_process_module/1/p3',), 'product_module/1', 'mrp/p3', 10),
-      (('business_process_module/1/p3',), 'product_module/4', 'mrp/p3', 40),
-      (('business_process_module/1/p3',), 'product_module/5', 'mrp/p3', 10),
-      (('business_process_module/1/p3',), 'product_module/1', None, -10)])
+      ((path_p2,), 'product_module/5', produced_organisations, 'mrp/p3', -10),
+      ((path_p2,), 'product_module/1', consumed_organisations, 'mrp/p2', 30),
+      ((path_p2,), 'product_module/2', consumed_organisations, 'mrp/p2', 10),
+      ((path_p3,), 'product_module/5', consumed_organisations, 'mrp/p3', 10),
+      ((path_p3,), 'product_module/3', consumed_organisations, 'mrp/p3', 40),
+      ((path_p3,), 'product_module/4', consumed_organisations, 'mrp/p3', 10),
+      ((path_p3,), 'product_module/5', produced_organisations, None, -10)])
     movement_list = applied_rule.objectValues()
     self.assertEquals(len(expected_value_set), len(movement_list))
     movement_value_set = set([])
     for movement in movement_list:
       movement_value_set |= set([(tuple(movement.getCausalityList()),
                                   movement.getResource(),
+                                  (movement.getSourceSection(),
+                                   movement.getSource(),
+                                   movement.getDestinationSection(),
+                                   movement.getDestination(),), # organisations
                                   movement.getTradePhase(),
                                   movement.getQuantity())])
     self.assertEquals(expected_value_set, movement_value_set)
 
-    """
-      Concurrent case
-    """
+  def test_TransformationRule_expand_concurrent(self):
     business_process = self.createConcurrentBusinessProcess()
-    order.edit(specialise_value=business_process)
+
+    # mock order
+    order = self.createDefaultOrder(business_process=business_process)
+    order_line = order.objectValues()[0]
+
+    # phases
+    phase_p2 = '%s/p2' % business_process.getRelativeUrl()
+    phase_p3 = '%s/p3' % business_process.getRelativeUrl()
+
+    # organisations
+    path = business_process.objectValues(
+      portal_type=self.portal.getPortalBusinessPathTypeList())[0]
+    source_section = path.getSourceSection()
+    source = path.getSource()
+    destination_section = path.getDestinationSection()
+    destination = path.getDestination()
+    organisations = (source_section, source, destination_section, destination)
+    consumed_organisations = (source_section, source, destination_section, None)
+    produced_organisations = (source_section, None, destination_section, destination)
+
+    # don't need another rules, just need TransformationRule for test
+    self.invalidateRules()
 
     self.stepTic()
 
@@ -254,7 +363,7 @@ class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
     applied_rule.edit(causality_value=order)
     movement.edit(order_value=order_line,
                   quantity=order_line.getQuantity(),
-                  resource=transformation.getResource())
+                  resource=order_line.getResource())
     # test mock
     applied_rule = movement.newContent(potal_type='Applied Rule')
 
@@ -263,59 +372,160 @@ class TestMRPImplementation(TestMRPMixin, ERP5TypeTestCase):
 
     # assertion
     expected_value_set = set([
-      (('business_process_module/2/p2',), 'product_module/2', 'mrp/p2', 30),
-      (('business_process_module/2/p2',), 'product_module/3', 'mrp/p2', 10),
-      (('business_process_module/2/p3',), 'product_module/4', 'mrp/p3', 40),
-      (('business_process_module/2/p3',), 'product_module/5', 'mrp/p3', 10),
-      (('business_process_module/2/p2', 'business_process_module/2/p3'), 'product_module/1', None, -10)])
+      ((phase_p2,), 'product_module/1', consumed_organisations, 'mrp/p2', 30),
+      ((phase_p2,), 'product_module/2', consumed_organisations, 'mrp/p2', 10),
+      ((phase_p3,), 'product_module/3', consumed_organisations, 'mrp/p3', 40),
+      ((phase_p3,), 'product_module/4', consumed_organisations, 'mrp/p3', 10),
+      ((phase_p2, phase_p3), 'product_module/5', produced_organisations, None, -10)])
     movement_list = applied_rule.objectValues()
     self.assertEquals(len(expected_value_set), len(movement_list))
     movement_value_set = set([])
     for movement in movement_list:
       movement_value_set |= set([(tuple(movement.getCausalityList()),
                                   movement.getResource(),
+                                  (movement.getSourceSection(),
+                                   movement.getSource(),
+                                   movement.getDestinationSection(),
+                                   movement.getDestination(),), # organisations
                                   movement.getTradePhase(),
                                   movement.getQuantity())])
     self.assertEquals(expected_value_set, movement_value_set)
 
+  def test_TransformationRule_expand_reexpand(self):
     """
     test case of difference when any movement are frozen
     by using above result
     """
-    # update relation
+    self.test_TransformationRule_expand_concurrent()
+
     self.stepTic()
 
-    for movement in movement_list:
+    applied_rule = self.portal.portal_simulation.objectValues()[0]
+
+    business_process = applied_rule.getCausalityValue().getSpecialiseValue()
+
+    # phases
+    phase_p2 = '%s/p2' % business_process.getRelativeUrl()
+    phase_p3 = '%s/p3' % business_process.getRelativeUrl()
+
+    # organisations
+    path = business_process.objectValues(
+      portal_type=self.portal.getPortalBusinessPathTypeList())[0]
+    source_section = path.getSourceSection()
+    source = path.getSource()
+    destination_section = path.getDestinationSection()
+    destination = path.getDestination()
+    consumed_organisations = (source_section, source, destination_section, None)
+    produced_organisations = (source_section, None, destination_section, destination)
+
+    movement = applied_rule.objectValues()[0]
+    applied_rule = movement.objectValues()[0]
+
+    # these movements are made by transformation
+    for movement in applied_rule.objectValues():
       movement.edit(quantity=1)
-      # XXX change state isFrozen of movement to 1,
-      #     but I think this way might be wrong.
+      # set the state value of isFrozen to 1,
       movement._baseSetFrozen(1)
 
     # re-expand
+    rule = self.portal.portal_rules.default_transformation_rule
     rule.expand(applied_rule)
 
     # assertion
     expected_value_set = set([
-      (('business_process_module/2/p2',), 'product_module/2', 'mrp/p2', 1), # Frozen
-      (('business_process_module/2/p2',), 'product_module/2', 'mrp/p2', 29),
-      (('business_process_module/2/p2',), 'product_module/3', 'mrp/p2', 1), # Frozen
-      (('business_process_module/2/p2',), 'product_module/3', 'mrp/p2', 9),
-      (('business_process_module/2/p3',), 'product_module/4', 'mrp/p3', 1), # Frozen
-      (('business_process_module/2/p3',), 'product_module/4', 'mrp/p3', 39),
-      (('business_process_module/2/p3',), 'product_module/5', 'mrp/p3', 1), # Frozen
-      (('business_process_module/2/p3',), 'product_module/5', 'mrp/p3', 9),
-      (('business_process_module/2/p2', 'business_process_module/2/p3'), 'product_module/1', None, 1), # Frozen
-      (('business_process_module/2/p2', 'business_process_module/2/p3'), 'product_module/1', None, -11)])
+      ((phase_p2,), 'product_module/1', consumed_organisations, 'mrp/p2', 1), # Frozen
+      ((phase_p2,), 'product_module/1', consumed_organisations, 'mrp/p2', 29),
+      ((phase_p2,), 'product_module/2', consumed_organisations, 'mrp/p2', 1), # Frozen
+      ((phase_p2,), 'product_module/2', consumed_organisations, 'mrp/p2', 9),
+      ((phase_p3,), 'product_module/3', consumed_organisations, 'mrp/p3', 1), # Frozen
+      ((phase_p3,), 'product_module/3', consumed_organisations, 'mrp/p3', 39),
+      ((phase_p3,), 'product_module/4', consumed_organisations, 'mrp/p3', 1), # Frozen
+      ((phase_p3,), 'product_module/4', consumed_organisations, 'mrp/p3', 9),
+      ((phase_p2, phase_p3), 'product_module/5', produced_organisations, None, 1), # Frozen
+      ((phase_p2, phase_p3), 'product_module/5', produced_organisations, None, -11)])
     movement_list = applied_rule.objectValues()
     self.assertEquals(len(expected_value_set), len(movement_list))
     movement_value_set = set([])
     for movement in movement_list:
       movement_value_set |= set([(tuple(movement.getCausalityList()),
                                   movement.getResource(),
+                                  (movement.getSourceSection(),
+                                   movement.getSource(),
+                                   movement.getDestinationSection(),
+                                   movement.getDestination(),), # organisations
                                   movement.getTradePhase(),
                                   movement.getQuantity())])
     self.assertEquals(expected_value_set, movement_value_set)
 
+  def test_TransformationSourcingRule_expand(self):
+    # mock order
+    order = self.createDefaultOrder()
+    order_line = order.objectValues()[0]
+
+    # don't need another rules, just need TransformationSourcingRule for test
+    self.invalidateRules()
+
+    self.stepTic()
+
+    business_process = order.getSpecialiseValue()
+
+    # get last path of a business process
+    # in simple business path, the last is between "partial_produced" and "done"
+    causality_path = None
+    for state in business_process.objectValues(
+      portal_type=self.portal.getPortalBusinessStateTypeList()):
+      if len(state.getRemainingTradePhaseList(self.portal)) == 0:
+        causality_path = state.getSuccessorRelatedValue()
+
+    # phases
+    phase_p2 = '%s/p2' % business_process.getRelativeUrl()
+
+    # organisations
+    source_section = causality_path.getSourceSection()
+    source = causality_path.getSource()
+    destination_section = causality_path.getDestinationSection()
+    destination = causality_path.getDestination()
+    organisations = (source_section, source, destination_section, destination)
+
+    # sourcing resource
+    sourcing_resource = order_line.getResource()
+
+    # alter simulations of the order
+    # root
+    applied_rule = self.portal.portal_simulation.newContent(portal_type='Applied Rule')
+    movement = applied_rule.newContent(portal_type='Simulation Movement')
+    applied_rule.edit(causality_value=order)
+    movement.edit(order_value=order_line,
+                  causality_value=causality_path,
+                  quantity=order_line.getQuantity(),
+                  resource=sourcing_resource,
+                  )
+
+    self.stepTic()
+
+    # test mock
+    applied_rule = movement.newContent(potal_type='Applied Rule')
+
+    rule = self.portal.portal_rules.default_transformation_sourcing_rule
+    rule.expand(applied_rule)
+
+    # assertion
+    expected_value_set = set([
+      ((phase_p2,), sourcing_resource, organisations, 10)])
+    movement_list = applied_rule.objectValues()
+    self.assertEquals(len(expected_value_set), len(movement_list))
+    movement_value_set = set([])
+    for movement in movement_list:
+      movement_value_set |= set([(tuple(movement.getCausalityList()),
+                                  movement.getResource(),
+                                  (movement.getSourceSection(),
+                                   movement.getSource(),
+                                   movement.getDestinationSection(),
+                                   movement.getDestination(),), # organisations
+                                  movement.getQuantity())])
+    self.assertEquals(expected_value_set, movement_value_set)
+
+
 def test_suite():
   suite = unittest.TestSuite()
   suite.addTest(unittest.makeSuite(TestMRPImplementation))
-- 
2.30.9