From a5df6f55afcc6bb4dad78561d87e6700e6e78b9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Nowak?= <luke@nexedi.com>
Date: Thu, 23 Jul 2009 14:17:20 +0000
Subject: [PATCH]  - experimental version of BPM enabled builder base class

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@28155 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/BPMBuilder.py | 783 ++++++++++++++++++++++++++++
 1 file changed, 783 insertions(+)
 create mode 100644 product/ERP5/Document/BPMBuilder.py

diff --git a/product/ERP5/Document/BPMBuilder.py b/product/ERP5/Document/BPMBuilder.py
new file mode 100644
index 0000000000..17db218e6e
--- /dev/null
+++ b/product/ERP5/Document/BPMBuilder.py
@@ -0,0 +1,783 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    艁ukasz Nowak <luke@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly 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.ERP5.Document.Alarm import Alarm
+from Products.ERP5Type import Permissions, PropertySheet
+from Products.ERP5.MovementGroup import MovementGroupNode
+from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
+from Products.ERP5Type.CopySupport import CopyError, tryMethodCallWithTemporaryPermission
+from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
+from Acquisition import aq_parent, aq_inner
+
+class CollectError(Exception): pass
+class MatrixError(Exception): pass
+class DuplicatedPropertyDictKeysError(Exception): pass
+class SelectMethodError(Exception): pass
+
+class BPMBuilder(Alarm):
+  """Top class for builders.
+
+  WARNING: This is BPM evaluation of building approach.
+  WARNING: Do NOT use it in production environment.
+
+  There are two types of builders - global safe and global unsafe.
+
+  Global safe builders can be configured like alarms.
+
+  Global unsafe builders have to be invoked by passing restrictive parameters
+  to them, so they cannot behave like alarms, they have to be invoked from
+  scripts.
+
+  Global safe builders characteristics are described in erp5-Updated.Builder.Ideas
+
+  Scripts assumptions:
+
+   * simulation_select_method_id have to return non delivered movements - it
+     shall parse returned list to manually remove delivered movements
+  """
+
+  meta_type = 'ERP5 Builder'
+  portal_type = 'Builder'
+  security = ClassSecurityInfo()
+
+  # Default Properties
+  property_sheets = ( PropertySheet.Base
+                    , PropertySheet.XMLObject
+                    , PropertySheet.CategoryCore
+                    , PropertySheet.DublinCore
+                    , PropertySheet.Arrow
+                    , PropertySheet.Amount
+                    , PropertySheet.Comment
+                    , PropertySheet.DeliveryBuilder
+                    , PropertySheet.Alarm
+                    , PropertySheet.Periodicity
+                    )
+
+  security.declareProtected(Permissions.View, 'build')
+  def build(self, tag=None, input_movement_list=None,
+        existing_delivery_list=None, select_method_dict=None, **kwargs):
+    """Builds document according to self configuration mixed with passed parameters
+
+    Selecting parameters (like input movement list) might be passed directly
+    to builder, but if not passed builder is able to find those values by
+    itself.
+
+    select_method_dict - dictionary which will be passed to input movement
+      select method
+    """
+    # XXX: TODO backward compatibility with old parameters
+    if select_method_dict is None:
+      select_method_dict = {}
+    # Call a script before building
+    self.callBeforeBuildingScript()
+    # Select movements
+    if input_movement_list is None:
+      business_path_value_list = self.getRelatedBusinessPathValueList()
+      if len(business_path_value_list) > 0:
+        # use only Business Path related movements
+        select_method_dict['causality_uid'] = [q.getUid() for q in business_path_value_list]
+      # do search
+      input_movement_value_list = self.searchMovementList(
+        delivery_relative_url_list=existing_delivery_list,
+        **select_method_dict)
+    else:
+      # movements were passed directly
+      input_movement_value_list = [self.unrestrictedTraverse(relative_url) for
+          relative_url in input_movement_list]
+    # Collect
+    root_group_node = self.collectMovement(input_movement_value_list)
+    # Build
+    delivery_value_list = self.buildDeliveryList(
+                       root_group_node,
+                       delivery_relative_url_list=existing_delivery_list,
+                       movement_list=input_movement_value_list)
+    # Call a script after building
+    self.callAfterBuildingScript(delivery_value_list,
+        input_movement_value_list)
+    return delivery_value_list
+
+  security.declareProtected(Permissions.View, 'activeSense')
+  def activeSense(self):
+    """Activate building, only one builder at time"""
+    self.setNextAlarmDate()
+    # A tag is provided as a parameter in order to be
+    # able to notify the user after all processes are ended
+    # Tag is generated from portal_ids so that it can be retrieved
+    # later when creating an active process for example
+    tag = str(self.portal_ids.generateNewLengthId(id_group=self.getId()))
+    self.activate(tag=tag).build(tag=tag)
+
+    if self.isAlarmNotificationMode():
+      self.activate(after_tag=tag).notify(include_active=True)
+
+  def searchMovementList(self, *args, **kw):
+    """
+      defines how to query all input movements which meet certain criteria
+      First, select movement matching to criteria define on Builder
+    """
+    searchMovementList = UnrestrictedMethod(self._searchMovementList)
+    return searchMovementList(*args, **kw)
+
+  def _searchMovementList(self, **kw):
+    """This method is wrapped by UnrestrictedMethod."""
+    input_movement_value_list = []
+    # We only search Simulation Movement - Luke do not know why...
+    kw['portal_type'] = 'Simulation Movement' # blah!
+
+    select_method = getattr(self.getPortalObject(),
+        self.simulation_select_method_id or
+        self._getTypeBasedMethod('_selectDefaultMovement'))
+    input_movement_value_list = select_method(**kw)
+
+    movement_dict = {}
+    for movement in input_movement_value_list:
+      if not movement.isBuildable():
+        raise ValueError('Movement %s is not buildable' % movement.getRelativeUrl())
+      if movement_dict.has_key(movement):
+        # if duplicated - fail
+        raise SelectMethodError("%s repeated %s in list" %
+                    (str(self.simulation_select_method_id),
+                    str(movement.getRelativeUrl())))
+      else:
+        movement_dict[movement] = 1
+
+    return input_movement_value_list
+
+  def _setDeliveryMovementProperties(self, delivery_movement,
+                                     input_movement, property_dict,
+                                     update_existing_movement=0,
+                                     force_update=0, activate_kw=None):
+    """
+      Initialize or update delivery movement properties.
+      Set delivery ratio on simulation movement.
+    """
+
+    """force_update is calculated in cryptic way
+       in new implementation we will be know if delivery is still modifiable by
+       builder or not
+    """
+    if update_existing_movement == 1 and not force_update:
+#      # Important.
+#      # Attributes of object_to_update must not be modified here.
+#      # Because we can not change values that user modified.
+#      # Delivery will probably diverge now, but this is not the job of
+#      # DeliveryBuilder to resolve such problem.
+#      # Use Solver instead.
+      if getattr(input_movement, 'setDeliveryRatio', None) is not None:
+        input_movement.edit(delivery_ratio=0)
+    else:
+      # Update quantity on movement
+      # XXX hardcoded value
+      property_dict['quantity'] = delivery_movement.getQuantity() + \
+          input_movement.getQuantity() # float point
+      property_dict['price'] = input_movement.getPrice() or 0.0
+      delivery_movement_price = delivery_movement.getPrice()
+      if delivery_movement_price is not None:
+        # doSomethingWithPrice - this is only example
+        property_dict['price'] = (property_dict['price'] \
+            + delivery_movement_price) / 2
+
+      # Update properties on object (quantity, price...)
+      delivery_movement._edit(force_update=1, **property_dict)
+      if getattr(input_movement, 'setDeliveryRatio', None) is not None:
+        if delivery_movement.getQuantity() in [0, 0.0, None]:
+          delivery_ratio = 1
+        else:
+          delivery_ratio = input_movement.getQuantity() / delivery_movement \
+              .getQuantity() # float point
+        input_movement.edit(delivery_ratio=delivery_ratio)
+    if getattr(input_movement,'setDeliveryValue', None) is not None:
+      input_movement.edit(delivery_value=delivery_movement,
+                               activate_kw=activate_kw)
+
+  def getRelatedBusinessPathValueList(self):
+    return self.getDeliveryBuilderRelatedValueList(
+        portal_type='Business Path') + self.getOrderBuilderRelatedValueList(
+        portal_type='Business Path')
+
+  def callBeforeBuildingScript(self):
+    """
+      Call a script on the module, for example, to remove some
+      auto_planned Order.
+      This part can only be done with a script, because user may want
+      to keep existing auto_planned Order, and only update lines in
+      them.
+      No activities are used when deleting a object, so, current
+      implementation should be OK.
+    """
+    delivery_module_before_building_script_id = \
+        self.getDeliveryModuleBeforeBuildingScriptId()
+    if delivery_module_before_building_script_id not in ["", None]:
+      delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule())
+      getattr(delivery_module, delivery_module_before_building_script_id)()
+
+  def collectMovement(self, movement_list):
+    """
+      group movements in the way we want. Thanks to this method, we are able
+      to retrieve movement classed by order, resource, criterion,....
+      movement_list : the list of movement wich we want to group
+      class_list : the list of classes used to group movements. The order
+                   of the list is important and determines by what we will
+                   group movement first
+                   Typically, check_list is :
+                   [DateMovementGroup,PathMovementGroup,...]
+    """
+    movement_group_list = self.getMovementGroupList()
+    last_line_movement_group = self.getDeliveryMovementGroupList()[-1]
+    separate_method_name_list = self.getDeliveryCellSeparateOrderList([])
+    root_group_node = MovementGroupNode(
+      separate_method_name_list=separate_method_name_list,
+      movement_group_list=movement_group_list,
+      last_line_movement_group=last_line_movement_group)
+    root_group_node.append(movement_list)
+    return root_group_node
+
+  def _test(self, instance, movement_group_node_list,
+                    divergence_list):
+    """XXX TODO docstring"""
+    result = True
+    new_property_dict = {}
+    for movement_group_node in movement_group_node_list:
+      tmp_result, tmp_property_dict = movement_group_node.test(
+        instance, divergence_list)
+      if not tmp_result:
+        result = tmp_result
+      new_property_dict.update(tmp_property_dict)
+    return result, new_property_dict
+
+  def _findUpdatableObject(self, instance_list, movement_group_node_list,
+                           divergence_list):
+    """XXX TODO docstring"""
+    instance = None
+    property_dict = {}
+    if not len(instance_list):
+      for movement_group_node in movement_group_node_list:
+        for k,v in movement_group_node.getGroupEditDict().iteritems():
+          if k in property_dict:
+            raise DuplicatedPropertyDictKeysError(k)
+          else:
+            property_dict[k] = v
+    else:
+      # we want to check the original delivery first.
+      # so sort instance_list by that current is exists or not.
+      try:
+        current = movement_group_node_list[-1].getMovementList()[0].getDeliveryValue()
+        portal = self.getPortalObject()
+        while current != portal:
+          if current in instance_list:
+            instance_list.sort(key=lambda x: x != current and 1 or 0)
+            break
+          current = current.getParentValue()
+      except AttributeError:
+        pass
+      for instance_to_update in instance_list:
+        result, property_dict = self._test(
+          instance_to_update, movement_group_node_list, divergence_list)
+        if result == True:
+          instance = instance_to_update
+          break
+    return instance, property_dict
+
+  def buildDeliveryList(self, *args, **kw):
+    """
+      Build deliveries from a list of movements
+    """
+    buildDeliveryList = UnrestrictedMethod(self._buildDeliveryList)
+    return buildDeliveryList(*args, **kw)
+
+  def _buildDeliveryList(self, movement_group_node, delivery_relative_url_list=None,
+                         movement_list=None,**kw):
+    """This method is wrapped by UnrestrictedMethod. XXX do docstring which have a sense"""
+    # Parameter initialization
+    if delivery_relative_url_list is None:
+      delivery_relative_url_list = []
+    if movement_list is None:
+      movement_list = []
+    # Module where we can create new deliveries
+    portal = self.getPortalObject()
+    delivery_module = getattr(portal, self.getDeliveryModule())
+    delivery_to_update_list = [portal.restrictedTraverse(relative_url) for \
+                               relative_url in delivery_relative_url_list]
+    # Deliveries we are trying to update
+    delivery_select_method_id = self.getDeliverySelectMethodId()
+    if delivery_select_method_id not in ["", None]:
+      to_update_delivery_sql_list = getattr(self, delivery_select_method_id) \
+                                      (movement_list=movement_list)
+      delivery_to_update_list.extend([sql_delivery.getObject() \
+                                     for sql_delivery \
+                                     in to_update_delivery_sql_list])
+    # We do not want to update the same object more than twice in one
+    # _deliveryGroupProcessing().
+    self._resetUpdated()
+    delivery_list = self._processDeliveryGroup(
+                          delivery_module,
+                          movement_group_node,
+                          self.getDeliveryMovementGroupList(),
+                          delivery_to_update_list=delivery_to_update_list,
+                          **kw)
+    return delivery_list
+
+  def _processDeliveryGroup(self, delivery_module, movement_group_node,
+                            collect_order_list, movement_group_node_list=None,
+                            delivery_to_update_list=None,
+                            divergence_list=None,
+                            activate_kw=None, force_update=0, **kw):
+    """
+      Build delivery from a list of movement
+    """
+    if movement_group_node_list is None:
+      movement_group_node_list = []
+    if divergence_list is None:
+      divergence_list = []
+    # do not use 'append' or '+=' because they are destructive.
+    movement_group_node_list = movement_group_node_list + [movement_group_node]
+    # Parameter initialization
+    if delivery_to_update_list is None:
+      delivery_to_update_list = []
+    delivery_list = []
+
+    if len(collect_order_list):
+      # Get sorted movement for each delivery
+      for grouped_node in movement_group_node.getGroupList():
+        new_delivery_list = self._processDeliveryGroup(
+                              delivery_module,
+                              grouped_node,
+                              collect_order_list[1:],
+                              movement_group_node_list=movement_group_node_list,
+                              delivery_to_update_list=delivery_to_update_list,
+                              divergence_list=divergence_list,
+                              activate_kw=activate_kw,
+                              force_update=force_update)
+        delivery_list.extend(new_delivery_list)
+        force_update = 0
+    else:
+      # Test if we can update a existing delivery, or if we need to create
+      # a new one
+      delivery_to_update_list = [
+        x for x in delivery_to_update_list \
+        if x.getPortalType() == self.getDeliveryPortalType() and \
+        not self._isUpdated(x, 'delivery')]
+      delivery, property_dict = self._findUpdatableObject(
+        delivery_to_update_list, movement_group_node_list,
+        divergence_list)
+
+      # if all deliveries are rejected in case of update, we update the
+      # first one.
+      if force_update and delivery is None and len(delivery_to_update_list):
+        delivery = delivery_to_update_list[0]
+
+      if delivery is None:
+        # Create delivery
+        try:
+          old_delivery = self._searchUpByPortalType(
+            movement_group_node.getMovementList()[0].getDeliveryValue(),
+            self.getDeliveryPortalType())
+        except AttributeError:
+          old_delivery = None
+        if old_delivery is None:
+          # from scratch
+          new_delivery_id = str(delivery_module.generateNewId())
+          delivery = delivery_module.newContent(
+            portal_type=self.getDeliveryPortalType(),
+            id=new_delivery_id,
+            created_by_builder=1,
+            activate_kw=activate_kw)
+        else:
+          # from duplicated original delivery
+          cp = tryMethodCallWithTemporaryPermission(
+            delivery_module, 'Copy or Move',
+            lambda parent, *ids:
+            parent._duplicate(parent.manage_copyObjects(ids=ids))[0],
+            (delivery_module, old_delivery.getId()), {}, CopyError)
+          delivery = delivery_module[cp['new_id']]
+          # delete non-split movements
+          keep_id_list = [y.getDeliveryValue().getId() for y in \
+                          movement_group_node.getMovementList()]
+          delete_id_list = [x.getId() for x in delivery.contentValues() \
+                           if x.getId() not in keep_id_list]
+          delivery.deleteContent(delete_id_list)
+      # Put properties on delivery
+      self._setUpdated(delivery, 'delivery')
+      if property_dict:
+        delivery.edit(**property_dict)
+
+      # Then, create delivery line
+      for grouped_node in movement_group_node.getGroupList():
+        self._processDeliveryLineGroup(
+                                delivery,
+                                grouped_node,
+                                self.getDeliveryLineMovementGroupList()[1:],
+                                divergence_list=divergence_list,
+                                activate_kw=activate_kw,
+                                force_update=force_update)
+      delivery_list.append(delivery)
+    return delivery_list
+
+  def _processDeliveryLineGroup(self, delivery, movement_group_node,
+                                collect_order_list, movement_group_node_list=None,
+                                divergence_list=None,
+                                activate_kw=None, force_update=0, **kw):
+    """
+      Build delivery line from a list of movement on a delivery
+    """
+    if movement_group_node_list is None:
+      movement_group_node_list = []
+    if divergence_list is None:
+      divergence_list = []
+    # do not use 'append' or '+=' because they are destructive.
+    movement_group_node_list = movement_group_node_list + [movement_group_node]
+
+    if len(collect_order_list) and not movement_group_node.getCurrentMovementGroup().isBranch():
+      # Get sorted movement for each delivery line
+      for grouped_node in movement_group_node.getGroupList():
+        self._processDeliveryLineGroup(
+          delivery,
+          grouped_node,
+          collect_order_list[1:],
+          movement_group_node_list=movement_group_node_list,
+          divergence_list=divergence_list,
+          activate_kw=activate_kw,
+          force_update=force_update)
+    else:
+      # Test if we can update an existing line, or if we need to create a new
+      # one
+      delivery_line_to_update_list = [x for x in delivery.contentValues(
+        portal_type=self.getDeliveryLinePortalType()) if \
+                                      not self._isUpdated(x, 'line')]
+      delivery_line, property_dict = self._findUpdatableObject(
+        delivery_line_to_update_list, movement_group_node_list,
+        divergence_list)
+      if delivery_line is not None:
+        update_existing_line = 1
+      else:
+        # Create delivery line
+        update_existing_line = 0
+        try:
+          old_delivery_line = self._searchUpByPortalType(
+            movement_group_node.getMovementList()[0].getDeliveryValue(),
+            self.getDeliveryLinePortalType())
+        except AttributeError:
+          old_delivery_line = None
+        if old_delivery_line is None:
+          # from scratch
+          new_delivery_line_id = str(delivery.generateNewId())
+          delivery_line = delivery.newContent(
+            portal_type=self.getDeliveryLinePortalType(),
+            id=new_delivery_line_id,
+            variation_category_list=[],
+            activate_kw=activate_kw)
+        else:
+          # from duplicated original line
+          cp = tryMethodCallWithTemporaryPermission(
+            delivery, 'Copy or Move',
+            lambda parent, *ids:
+            parent._duplicate(parent.manage_copyObjects(ids=ids))[0],
+            (delivery, old_delivery_line.getId()), {}, CopyError)
+          delivery_line = delivery[cp['new_id']]
+          # reset variation category list
+          delivery_line.setVariationCategoryList([])
+          # delete non-split movements
+          keep_id_list = [y.getDeliveryValue().getId() for y in \
+                          movement_group_node.getMovementList()]
+          delete_id_list = [x.getId() for x in delivery_line.contentValues() \
+                           if x.getId() not in keep_id_list]
+          delivery_line.deleteContent(delete_id_list)
+      # Put properties on delivery line
+      self._setUpdated(delivery_line, 'line')
+      if property_dict:
+        delivery_line.edit(**property_dict)
+
+      if movement_group_node.getCurrentMovementGroup().isBranch():
+        for grouped_node in movement_group_node.getGroupList():
+          self._processDeliveryLineGroup(
+            delivery_line,
+            grouped_node,
+            collect_order_list[1:],
+            movement_group_node_list=movement_group_node_list,
+            divergence_list=divergence_list,
+            activate_kw=activate_kw,
+            force_update=force_update)
+        return
+
+      # Update variation category list on line
+      variation_category_dict = dict([(variation_category, True) for
+                                      variation_category in
+                                      delivery_line.getVariationCategoryList()])
+      for movement in movement_group_node.getMovementList():
+        for category in movement.getVariationCategoryList():
+          variation_category_dict[category] = True
+      variation_category_list = sorted(variation_category_dict.keys())
+      delivery_line.setVariationCategoryList(variation_category_list)
+      # Then, create delivery movement (delivery cell or complete delivery
+      # line)
+      grouped_node_list = movement_group_node.getGroupList()
+      # If no group is defined for cell, we need to continue, in order to
+      # save the quantity value
+      if len(grouped_node_list):
+        for grouped_node in grouped_node_list:
+          self._processDeliveryCellGroup(
+                                    delivery_line,
+                                    grouped_node,
+                                    self.getDeliveryCellMovementGroupList()[1:],
+                                    update_existing_line=update_existing_line,
+                                    divergence_list=divergence_list,
+                                    activate_kw=activate_kw,
+                                    force_update=force_update)
+      else:
+        self._processDeliveryCellGroup(
+                                  delivery_line,
+                                  movement_group_node,
+                                  [],
+                                  update_existing_line=update_existing_line,
+                                  divergence_list=divergence_list,
+                                  activate_kw=activate_kw,
+                                  force_update=force_update)
+
+
+  def _processDeliveryCellGroup(self, delivery_line, movement_group_node,
+                                collect_order_list, movement_group_node_list=None,
+                                update_existing_line=0,
+                                divergence_list=None,
+                                activate_kw=None, force_update=0):
+    """
+      Build delivery cell from a list of movement on a delivery line
+      or complete delivery line
+    """
+    if movement_group_node_list is None:
+      movement_group_node_list = []
+    if divergence_list is None:
+      divergence_list = []
+    # do not use 'append' or '+=' because they are destructive.
+    movement_group_node_list = movement_group_node_list + [movement_group_node]
+
+    if len(collect_order_list):
+      # Get sorted movement for each delivery line
+      for grouped_node in movement_group_node.getGroupList():
+        self._processDeliveryCellGroup(
+          delivery_line,
+          grouped_node,
+          collect_order_list[1:],
+          movement_group_node_list=movement_group_node_list,
+          update_existing_line=update_existing_line,
+          divergence_list=divergence_list,
+          activate_kw=activate_kw,
+          force_update=force_update)
+    else:
+      movement_list = movement_group_node.getMovementList()
+      if len(movement_list) != 1:
+        raise CollectError, "DeliveryBuilder: %s unable to distinct those\
+              movements: %s" % (self.getId(), str(movement_list))
+      else:
+        # XXX Hardcoded value
+        base_id = 'movement'
+        object_to_update = None
+        # We need to initialize the cell
+        update_existing_movement = 0
+        movement = movement_list[0]
+        # decide if we create a cell or if we update the line
+        # Decision can only be made with line matrix range:
+        # because matrix range can be empty even if line variation category
+        # list is not empty
+        property_dict = {}
+        if len(delivery_line.getCellKeyList(base_id=base_id)) == 0:
+          # update line
+          if update_existing_line == 1:
+            if self._isUpdated(delivery_line, 'cell'):
+              object_to_update_list = []
+            else:
+              object_to_update_list = [delivery_line]
+          else:
+            object_to_update_list = []
+          object_to_update, property_dict = self._findUpdatableObject(
+            object_to_update_list, movement_group_node_list,
+            divergence_list)
+          if object_to_update is not None:
+            update_existing_movement = 1
+          else:
+            object_to_update = delivery_line
+        else:
+          object_to_update_list = [
+            delivery_line.getCell(base_id=base_id, *cell_key) for cell_key in \
+            delivery_line.getCellKeyList(base_id=base_id) \
+            if delivery_line.hasCell(base_id=base_id, *cell_key)]
+          object_to_update, property_dict = self._findUpdatableObject(
+            object_to_update_list, movement_group_node_list,
+            divergence_list)
+          if object_to_update is not None:
+            # We update a existing cell
+            # delivery_ratio of new related movement to this cell
+            # must be updated to 0.
+            update_existing_movement = 1
+
+        if object_to_update is None:
+          # create a new cell
+          cell_key = movement.getVariationCategoryList(
+              omit_optional_variation=1)
+          if not delivery_line.hasCell(base_id=base_id, *cell_key):
+            try:
+              old_cell = movement_group_node.getMovementList()[0].getDeliveryValue()
+            except AttributeError:
+              old_cell = None
+            if old_cell is None:
+              # from scratch
+              cell = delivery_line.newCell(base_id=base_id, \
+                       portal_type=self.getDeliveryCellPortalType(),
+                       activate_kw=activate_kw,*cell_key)
+            else:
+              # from duplicated original line
+              cp = tryMethodCallWithTemporaryPermission(
+                delivery_line, 'Copy or Move',
+                lambda parent, *ids:
+                parent._duplicate(parent.manage_copyObjects(ids=ids))[0],
+                (delivery_line, old_cell.getId()), {}, CopyError)
+              cell = delivery_line[cp['new_id']]
+
+            vcl = movement.getVariationCategoryList()
+            cell._edit(category_list=vcl,
+                      # XXX hardcoded value
+                      mapped_value_property_list=['quantity', 'price'],
+                      membership_criterion_category_list=vcl,
+                      membership_criterion_base_category_list=movement.\
+                                             getVariationBaseCategoryList())
+            object_to_update = cell
+          else:
+            raise MatrixError, 'Cell: %s already exists on %s' % \
+                  (str(cell_key), str(delivery_line))
+        self._setUpdated(object_to_update, 'cell')
+        self._setDeliveryMovementProperties(
+                            object_to_update, movement, property_dict,
+                            update_existing_movement=update_existing_movement,
+                            force_update=force_update, activate_kw=activate_kw)
+
+  def callAfterBuildingScript(self, *args, **kw):
+    """
+      Call script on each delivery built.
+    """
+    callAfterBuildingScript = UnrestrictedMethod(self._callAfterBuildingScript)
+    return callAfterBuildingScript(*args, **kw)
+
+  def _callAfterBuildingScript(self, delivery_list, movement_list=None, **kw):
+    """
+      Call script on each delivery built.
+      This method is wrapped by UnrestrictedMethod.
+    """
+    if not len(delivery_list):
+      return
+    # Parameter initialization
+    if movement_list is None:
+      movement_list = []
+    delivery_after_generation_script_id = \
+                              self.getDeliveryAfterGenerationScriptId()
+    related_simulation_movement_path_list = \
+                              [x.getPath() for x in movement_list]
+    if delivery_after_generation_script_id not in ["", None]:
+      for delivery in delivery_list:
+        script = getattr(delivery, delivery_after_generation_script_id)
+        # BBB: Only Python Scripts were used in the past, and they might not
+        # accept an arbitrary argument. So to keep compatibility,
+        # check if it can take the new parameter safely, only when
+        # the callable object is a Python Script.
+        safe_to_pass_parameter = True
+        meta_type = getattr(script, 'meta_type', None)
+        if meta_type == 'Script (Python)':
+          # check if the script accepts related_simulation_movement_path_list
+          safe_to_pass_parameter = False
+          for param in script.params().split(','):
+            param = param.split('=', 1)[0].strip()
+            if param == 'related_simulation_movement_path_list' \
+                    or param.startswith('**'):
+              safe_to_pass_parameter = True
+              break
+
+        if safe_to_pass_parameter:
+          script(related_simulation_movement_path_list=related_simulation_movement_path_list)
+        else:
+          script()
+
+  security.declareProtected(Permissions.AccessContentsInformation,
+                           'getMovementGroupList')
+  def getMovementGroupList(self, portal_type=None, collect_order_group=None,
+                            **kw):
+    """
+    Return a list of movement groups sorted by collect order group and index.
+    """
+    category_index_dict = {}
+    for i in self.getPortalObject().portal_categories.collect_order_group.contentValues():
+      category_index_dict[i.getId()] = i.getIntIndex()
+
+    def sort_movement_group(a, b):
+        return cmp(category_index_dict.get(a.getCollectOrderGroup()),
+                   category_index_dict.get(b.getCollectOrderGroup())) or \
+               cmp(a.getIntIndex(), b.getIntIndex())
+    if portal_type is None:
+      portal_type = self.getPortalMovementGroupTypeList()
+    movement_group_list = [x for x in self.contentValues(filter={'portal_type': portal_type}) \
+                           if collect_order_group is None or collect_order_group == x.getCollectOrderGroup()]
+    return sorted(movement_group_list, sort_movement_group)
+
+  # XXX category name is hardcoded.
+  def getDeliveryMovementGroupList(self, **kw):
+    return self.getMovementGroupList(collect_order_group='delivery')
+
+  # XXX category name is hardcoded.
+  def getDeliveryLineMovementGroupList(self, **kw):
+    return self.getMovementGroupList(collect_order_group='line')
+
+  # XXX category name is hardcoded.
+  def getDeliveryCellMovementGroupList(self, **kw):
+    return self.getMovementGroupList(collect_order_group='cell')
+
+  def _searchUpByPortalType(self, obj, portal_type):
+    limit_portal_type = self.getPortalObject().getPortalType()
+    while obj is not None:
+      obj_portal_type = obj.getPortalType()
+      if obj_portal_type == portal_type:
+        break
+      elif obj_portal_type == limit_portal_type:
+        obj = None
+        break
+      else:
+        obj = aq_parent(aq_inner(obj))
+    return obj
+
+  def _isUpdated(self, obj, level):
+    tv = getTransactionalVariable(self)
+    return level in tv['builder_processed_list'].get(obj, [])
+
+  def _setUpdated(self, obj, level):
+    tv = getTransactionalVariable(self)
+    if tv.get('builder_processed_list', None) is None:
+      self._resetUpdated()
+    tv['builder_processed_list'][obj] = \
+       tv['builder_processed_list'].get(obj, []) + [level]
+
+  def _resetUpdated(self):
+    tv = getTransactionalVariable(self)
+    tv['builder_processed_list'] = {}
-- 
2.30.9