BusinessPath.py 34 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4 5
##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
6
#                    Yusuke Muraoka <yusuke@nexedi.com>
7
#                    Łukasz Nowak <luke@nexedi.com>
8 9
#
# WARNING: This program as such is intended to be used by professional
10
# programmers who take the whole responsibility of assessing all potential
11 12
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
13
# guarantees and support are strongly advised to contract a Free Software
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
# 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

Łukasz Nowak's avatar
Łukasz Nowak committed
34
from Products.ERP5Type import Permissions, PropertySheet, interfaces
35
from Products.ERP5.Document.Path import Path
36
from Products.ERP5Type.Core.Predicate import Predicate
37 38 39

import zope.interface

40
class BusinessPath(Path, Predicate):
41
  """
42
    The BusinessPath class embeds all information related to
43
    lead times and parties involved at a given phase of a business
44 45
    process.

46
    BusinessPath are also used as helper to build deliveries from
47 48
    buildable movements.

49 50
    The idea is to invoke isBuildable() on the collected simulation
    movements (which are orphan) during build "after select" process
51

52
    Here is the typical code of an alarm in charge of the building process::
53

54 55 56
      builder = portal_deliveries.a_delivery_builder
      for business_path in builder.getDeliveryBuilderRelatedValueList():
        builder.build(causality_uid=business_path.getUid(),) # Select movements
57

58
      Pros: global select is possible by not providing a causality_uid
59
      Cons: global select retrieves long lists of orphan movements which
60 61
              are not yet buildable
            the build process could be rather slow or require activities
62

63
    TODO:
Łukasz Nowak's avatar
Łukasz Nowak committed
64 65
      - merge build implementation from erp5_bpm business template to ERP5
        product code with backward compatibility
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  """
  meta_type = 'ERP5 Business Path'
  portal_type = 'Business Path'

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.CategoryCore
                    , PropertySheet.DublinCore
                    , PropertySheet.Folder
                    , PropertySheet.Comment
                    , PropertySheet.Arrow
                    , PropertySheet.Chain
83
                    , PropertySheet.SortIndex
84
                    , PropertySheet.BusinessPath
85
                    , PropertySheet.Reference
86 87 88
                    )

  # Declarative interfaces
89
  zope.interface.implements(interfaces.ICategoryAccessProvider,
90 91 92
                            interfaces.IArrowBase,
                            interfaces.IBusinessPath,
                            interfaces.IBusinessBuildable,
93 94
                            interfaces.IBusinessCompletable,
                            interfaces.IPredicate,
95
                            )
96

97
  # IArrowBase implementation
98 99 100
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getSourceArrowBaseCategoryList')
  def getSourceArrowBaseCategoryList(self):
101 102 103 104
    """
      Returns all categories which are used to define the source
      of this Arrow
    """
105
    # Naive implementation - we must use category groups instead - XXX
106 107 108
    return ('source',
            'source_account',
            'source_administration',
109 110 111
            #'source_advice',
            #'source_carrier',
            #'source_decision',
112 113 114
            'source_function',
            'source_payment',
            'source_project',
115
            #'source_referral',
116
            'source_section',
117 118 119
            #'source_trade',
            #'source_transport'
            )
120

121 122 123
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getDestinationArrowBaseCategoryList')
  def getDestinationArrowBaseCategoryList(self):
124 125 126 127
    """
      Returns all categories which are used to define the destination
      of this Arrow
    """
128
    # Naive implementation - we must use category groups instead - XXX
129 130 131
    return ('destination',
            'destination_account',
            'destination_administration',
132 133 134
            #'destination_advice',
            #'destination_carrier',
            #'destination_decision',
135 136 137
            'destination_function',
            'destination_payment',
            'destination_project',
138
            #'destination_referral',
139
            'destination_section',
140 141 142
            #'destination_trade',
            #'destination_transport'
            )
143

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getArrowCategoryDict')
  def getArrowCategoryDict(self, context=None, **kw):
    result = {}
    dynamic_category_list = self._getDynamicCategoryList(context)
    for base_category in self.getSourceArrowBaseCategoryList() +\
            self.getDestinationArrowBaseCategoryList():
      category_url_list = Path._getAcquiredCategoryMembershipList(
        self, base_category, **kw)
      if len(category_url_list) == 0 and context is not None:
        category_url_list = self._filterCategoryList(dynamic_category_list,
                                                     base_category, **kw)
      if len(category_url_list) > 0:
        result[base_category] = category_url_list
    return result

160
  # ICategoryAccessProvider overridden methods
161 162
  def _getCategoryMembershipList(self, category, **kw):
    """
163 164
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
165
    """
166
    context = kw.pop('context', None)
167
    result = Path._getCategoryMembershipList(self, category, **kw)
168 169
    if len(result) > 0:
      return result
170 171
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
172 173
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
174 175 176 177
    return result

  def _getAcquiredCategoryMembershipList(self, category, **kw):
    """
178 179
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
180
    """
181
    context = kw.pop('context', None)
182
    result = Path._getAcquiredCategoryMembershipList(self, category, **kw)
183 184
    if len(result) > 0:
      return result
185 186
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
187 188
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
189 190
    return result

191 192 193
  def _filterCategoryList(self, category_list, category, spec=(),
                          filter=None, portal_type=(), base=0,
                          keep_default=1, checked_permission=None):
194 195 196 197
    """
      XXX - implementation missing
      TBD - look at CategoryTool._buildFilter for inspiration
    """
198 199 200 201 202 203 204 205
    # basic filtering:
    #  * remove categories which base name is not category
    #  * respect base parameter
    prefix = category + '/'
    start_index = not base and len(prefix) or 0
    return [category[start_index:]
            for category in category_list
            if category.startswith(prefix)]
206 207 208

  # Dynamic context based categories
  def _getDynamicCategoryList(self, context):
209 210
    return self._getDynamicSourceCategoryList(context) \
         + self._getDynamicDestinationCategoryList(context)
211 212 213 214 215 216

  def _getDynamicSourceCategoryList(self, context):
    method_id = self.getSourceMethodId()
    if method_id:
      method = getattr(self, method_id)
      return method(context)
217
    return []
218 219 220 221 222 223

  def _getDynamicDestinationCategoryList(self, context):
    method_id = self.getDestinationMethodId()
    if method_id:
      method = getattr(self, method_id)
      return method(context)
224
    return []
225

226
  # IBusinessBuildable implementation
227
  def isBuildable(self, explanation):
228 229
    """
    """
230 231
    if self.isCompleted(explanation) or self.isFrozen(explanation):
      return False # No need to build what was already built or frozen
232 233 234

    # check if there is at least one simulation movement which is not
    # delivered
235
    for simulation_movement in self.getRelatedSimulationMovementValueList(
236 237
        explanation):
      if simulation_movement.getDeliveryValue() is None:
238
        break
239 240 241 242
    else:
      # if all simulation movements are delivered, we can bail out
      return False

243 244
    predecessor = self.getPredecessorValue()
    if predecessor is None:
245
      return True
246 247 248 249 250 251 252 253 254 255
    # XXX FIXME TODO
    # For now isPartiallyCompleted is used, as it was
    # assumed to not implement isPartiallyBuildable, so in reality
    # isBuildable is implemented like isPartiallyBuildable
    #
    # But in some cases it might be needed to implement
    # isPartiallyBuildable, than isCompleted have to be used here
    #
    # Such cases are Business Processes using sequence not related
    # to simulation tree with much of compensations
256
    return predecessor.isPartiallyCompleted(explanation)
257 258 259 260 261 262

  def isPartiallyBuildable(self, explanation):
    """
      Not sure if this will exist some day XXX
    """

263
  def _getExplanationUidList(self, explanation):
264
    """Helper method to fetch really explanation related movements"""
265
    explanation_uid_list = [explanation.getUid()]
266 267 268
    # XXX: getCausalityRelatedValueList is oversimplification, assumes
    #      that documents are sequenced like simulation movements, which
    #      is wrong
269
    for found_explanation in explanation.getCausalityRelatedValueList(
270 271
        portal_type=self.getPortalDeliveryTypeList()) + \
        explanation.getCausalityValueList():
272 273
      explanation_uid_list.extend(self._getExplanationUidList(
        found_explanation))
274 275
    return explanation_uid_list

276
  def build(self, explanation):
277 278 279
    """
      Build
    """
280
    builder_list = self.getDeliveryBuilderValueList() # Missing method
281
    for builder in builder_list:
282 283 284 285 286 287 288
      # chosen a way that builder is good enough to decide to select movements
      # which shall be really build (movement selection for build is builder
      # job, not business path job)
      builder.build(select_method_dict={
        'causality_uid': self.getUid(),
        'explanation_uid': self._getExplanationUidList(explanation)
      })
289 290

  # IBusinessCompletable implementation
291 292
  security.declareProtected(Permissions.AccessContentsInformation,
      'isCompleted')
293
  def isCompleted(self, explanation):
294 295 296 297
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
298
    acceptable_state_list = self.getCompletedStateList()
299
    for movement in self.getRelatedSimulationMovementValueList(explanation):
300 301 302 303
      if movement.getSimulationState() not in acceptable_state_list:
        return False
    return True

304 305
  security.declareProtected(Permissions.AccessContentsInformation,
      'isPartiallyCompleted')
306
  def isPartiallyCompleted(self, explanation):
307 308 309 310
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
311
    acceptable_state_list = self.getCompletedStateList()
312
    for movement in self.getRelatedSimulationMovementValueList(explanation):
313 314 315 316
      if movement.getSimulationState() in acceptable_state_list:
        return True
    return False

317 318
  security.declareProtected(Permissions.AccessContentsInformation,
      'isFrozen')
319
  def isFrozen(self, explanation):
320 321 322 323
    """
      Looks at all simulation related movements
      and checks if frozen
    """
324
    movement_list = self.getRelatedSimulationMovementValueList(explanation)
325 326 327 328 329 330 331
    if len(movement_list) == 0:
      return False # Nothing to be considered as Frozen
    for movement in movement_list:
      if not movement.isFrozen():
        return False
    return True

332 333 334 335 336 337 338 339 340 341 342
  def _recurseGetValueList(self, document, portal_type):
    """Helper method to recurse documents as deep as possible and returns
       list of document values matching portal_type"""
    return_list = []
    for subdocument in document.contentValues():
      if subdocument.getPortalType() == portal_type:
        return_list.append(subdocument)
      return_list.extend(self._recurseGetValueList(subdocument, portal_type))
    return return_list

  def isMovementRelatedWithMovement(self, movement_value_a, movement_value_b):
343
    """Checks if self is parent or children to movement_value
344

345 346 347 348 349 350 351 352 353 354 355 356 357
    This logic is Business Process specific for Simulation Movements, as
    sequence of Business Process is not related appearance of Simulation Tree

    movement_value_a, movement_value_b - movements to check relation between
    """
    movement_a_path = '%s/' % movement_value_a.getRelativeUrl()
    movement_b_path = '%s/' % movement_value_b.getRelativeUrl()

    if movement_a_path == movement_b_path or \
       movement_a_path.startswith(movement_b_path) or \
       movement_b_path.startswith(movement_a_path):
      return True
    return False
358

359 360
  def _isDeliverySimulationMovementRelated(self, simulation_movement,
                                           delivery_simulation_movement_list):
361 362
    """Helper method, which checks if simulation_movement is BPM like related
       with delivery"""
363
    for delivery_simulation_movement in delivery_simulation_movement_list:
364 365 366 367 368
      if self.isMovementRelatedWithMovement(delivery_simulation_movement,
          simulation_movement):
        return True
    return False

369
  # IBusinessPath implementation
370 371 372 373
  security.declareProtected(Permissions.AccessContentsInformation,
      'getRelatedSimulationMovementValueList')
  def getRelatedSimulationMovementValueList(self, explanation):
    """
374 375 376 377
      Returns recursively all Simulation Movements indirectly related to explanation and self

      As business sequence is not related to simulation tree need to built
      full simulation trees per applied rule
378
    """
379
    portal_catalog = self.getPortalObject().portal_catalog
380

381 382 383
    delivery_simulation_movement_list = portal_catalog(
      delivery_uid=[x.getUid() for x in explanation.getMovementList()])

384
    related_list = self.getBusinessPathClosure(delivery_simulation_movement_list)
385

386
    self_url = self.getRelativeUrl()
387 388 389
    return [m for m in related_list if m.getCausality() == self_url]

  def getBusinessPathClosure(self, movement_list):
Nicolas Dumazet's avatar
Nicolas Dumazet committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
    """
    Returns a list of Simulation Movement that are related to
    movement_list.
    "related" means that each of the returned Movement
    will be an ancestor or a descendant of a movement in movement_list

    Formally, this method returns all Simulation Movements in:
       ancestors(movement_list) U descendants(movement_list)
    """
    # We need to find all ancestors of movement_list, as well as all
    # of its descendants.
    # When A is an ancestor of B we have:
    #   ancestors(B) > ancestors(A) and
    # and
    #   descendants(A) > descendants(B)
    # In this sense it only makes sense to compute descendants of A
    # and ancestors of B.
    #
    # To do this we construct a tree of all (physical) paths leading
    # to each movement in movement_list. This tree can be seen
    # as a subtree of the whole Simulation Tree, or as a coloration
    # of the Simulation Tree.
    # Then, for each tree leaf, if that leaf has an non-root ancestor,
    # we remove the leaf and only keep the ancestor:
    # Because of the above properties,
    #   closure({leaf, ancestor}) == closure({ancestor})
    # which ensures that at the end of the coloration,
    #   closure(colored_tree) == closure(movement_list)
418 419 420 421
    colored_tree_dict = dict()

    leaf_marker = object()
    for simulation_movement in movement_list:
Nicolas Dumazet's avatar
Nicolas Dumazet committed
422 423
      # remove portal_simulation from the path
      component_list = simulation_movement.getRelativeUrl().split("/")[1:]
424 425

      cur = colored_tree_dict
Nicolas Dumazet's avatar
Nicolas Dumazet committed
426 427
      for component in component_list[:-1]:
        cur = cur.setdefault(component, {})
428 429 430 431 432
        if cur == leaf_marker:
          # an ancestor of simulation_movement was colored before
          break
      else:
        # note that we remove possibly-colored-before descendants
Nicolas Dumazet's avatar
Nicolas Dumazet committed
433
        cur[component_list[-1]] = leaf_marker
434 435

    related_list = []
Nicolas Dumazet's avatar
Nicolas Dumazet committed
436
    def closure(root, path_item_tree):
437
      """
Nicolas Dumazet's avatar
Nicolas Dumazet committed
438 439
      recursive helper filling related_list with:
        nodes(tree) U descendants(leafs(tree))
440

Nicolas Dumazet's avatar
Nicolas Dumazet committed
441
      root is a zodb object where the path_item_tree should be rooted.
442
      """
Nicolas Dumazet's avatar
Nicolas Dumazet committed
443
      for k, v in path_item_tree.iteritems():
444 445 446 447 448 449 450 451 452 453
        cur = root[k]
        # XXX maybe using parity Applied Rule / Simulation Movement is enough?
        if cur.getPortalType() == 'Simulation Movement':
          related_list.append(cur)
        if v == leaf_marker:
          related_list.extend(self._recurseGetValueList(cur, 'Simulation Movement'))
        else:
          closure(cur, v)
    closure(self.getPortalObject().portal_simulation, colored_tree_dict)
    return related_list
454

455 456 457 458 459 460 461 462 463 464 465 466 467 468
  def getExpectedQuantity(self, explanation, *args, **kwargs):
    """
      Returns the expected stop date for this
      path based on the explanation.

      XXX predecessor_quantity argument is required?
    """
    if self.getQuantity():
      return self.getQuantity()
    elif self.getEfficiency():
      return explanation.getQuantity() * self.getEfficiency()
    else:
      return explanation.getQuantity()

469
  def getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
470 471 472 473 474 475 476
    """
      Returns the expected start date for this
      path based on the explanation.

      predecessor_date -- if provided, computes the date base on the
                          date value provided
    """
477 478 479 480 481 482 483 484 485 486 487
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStartDate,
                                 self._getPredecessorExpectedStartDate,
                                 self._getSuccessorExpectedStartDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

  def _getRootExplanationExpectedStartDate(self, explanation, *args, **kwargs):
    if self.getParentValue().isStartDateReferential():
      return explanation.getStartDate()
    else:
488 489 490
      expected_date = self.getExpectedStopDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
491 492

  def _getPredecessorExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
493
    if predecessor_date is None:
494 495 496 497 498 499 500 501 502
      node = self.getPredecessorValue()
      if node is not None:
        predecessor_date = node.getExpectedCompletionDate(explanation, *args, **kwargs)
    if predecessor_date is not None:
      return predecessor_date + self.getWaitTime()

  def _getSuccessorExpectedStartDate(self, explanation, *args, **kwargs):
    node = self.getSuccessorValue()
    if node is not None:
503 504 505
      expected_date =  node.getExpectedBeginningDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
506 507

  def getExpectedStopDate(self, explanation, predecessor_date=None, *args, **kwargs):
508 509 510 511 512 513 514
    """
      Returns the expected stop date for this
      path based on the explanation.

      predecessor_date -- if provided, computes the date base on the
                          date value provided
    """
515 516 517 518 519 520 521 522 523 524 525
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStopDate,
                                 self._getPredecessorExpectedStopDate,
                                 self._getSuccessorExpectedStopDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

  def _getRootExplanationExpectedStopDate(self, explanation, *args, **kwargs):
    if self.getParentValue().isStopDateReferential():
      return explanation.getStopDate()
    else:
526 527 528
      expected_date = self.getExpectedStartDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getLeadTime()
529 530 531 532

  def _getPredecessorExpectedStopDate(self, explanation, *args, **kwargs):
    node = self.getPredecessorValue()
    if node is not None:
533 534 535
      expected_date = node.getExpectedCompletionDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getWaitTime() + self.getLeadTime()
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564

  def _getSuccessorExpectedStopDate(self, explanation, *args, **kwargs):
    node = self.getSuccessorValue()
    if node is not None:
      return node.getExpectedBeginningDate(explanation, *args, **kwargs)

  def _getExpectedDate(self, explanation, root_explanation_method,
                       predecessor_method, successor_method,
                       visited=None, *args, **kwargs):
    """
      Returns the expected stop date for this
      path based on the explanation.

      root_explanation_method -- used when the path is root explanation
      predecessor_method --- used to get expected date of side of predecessor
      successor_method --- used to get expected date of side of successor
      visited -- only used to prevent infinite recursion internally
    """
    if visited is None:
      visited = []

    # mark the path as visited
    if self not in visited:
      visited.append(self)

    if self.isDeliverable():
      return root_explanation_method(
        explanation, visited=visited, *args, **kwargs)

565 566
    predecessor_expected_date = predecessor_method(
      explanation, visited=visited, *args, **kwargs)
567

568 569
    successor_expected_date = successor_method(
      explanation, visited=visited, *args, **kwargs)
570 571 572 573 574 575 576 577 578 579 580 581 582

    if successor_expected_date is not None or \
       predecessor_expected_date is not None:
      # return minimum expected date but it is not None
      if successor_expected_date is None:
        return predecessor_expected_date
      elif predecessor_expected_date is None:
        return successor_expected_date
      else:
        if predecessor_expected_date < successor_expected_date:
          return predecessor_expected_date
        else:
          return successor_expected_date
583

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
  security.declareProtected(Permissions.AccessContentsInformation,
      'getBuildableMovementList')
  def getBuildableMovementList(self, **sql_kw):
    """
    Query catalog to find a list of movements related to this Business Path.

    Filter the results to return only Buildable Movements

    To specialize your builder, you can pass along SQL keywords
    in sql_kw, for instance:
      search_kw = {}
      search_kw['movement.quantity'] = {'query':0, 'range':'neq'}
      search_kw['movement.price'] = {'query':0, 'range':'neq'}
      sql_kw = portal_catalog.buildSQLQuery(**search_kw)
    """
599 600 601 602 603 604 605
    predecessor_state = self.getPredecessorValue()
    if predecessor_state is None:
      predecessor_uid_list = []
    else:
      predecessor_list = predecessor_state.getSuccessorRelatedValueList()
      predecessor_uid_list = [x.getUid() for x in predecessor_list]
    all_movement_list = self.BusinessPath_zSelectBuildableMovementList(
606
                          business_path_uid=self.getUid(),
607
                          predecessor_uid_list=predecessor_uid_list,
608 609 610 611 612 613 614 615 616
                          **sql_kw)

    # select method should return only non-delivered movements, but
    # maybe movements have been built in the meantime & catalog wasnt updated?
    non_delivered_movement_list = filter(lambda x:x.getDeliveryValue() is None,
                                         all_movement_list)

    return self.filterBuildableMovementList(non_delivered_movement_list)

617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
  security.declareProtected(Permissions.AccessContentsInformation,
      'filterBuildableMovementList')
  def filterBuildableMovementList(self, non_delivered_movement_list):
    """
    Given a list of non delivered movements that all have "self" as
    a causality value, return the ones that are buildables

    This is computed efficiently: movements are first separated into
    distinct closures, and then filtering is made on each closure.
    """
    predecessor_state = self.getPredecessorValue()
    if predecessor_state is None:
      # first Path in Process, all movements can be built
      return non_delivered_movement_list

    predecessor_to_state_dict = {}
    for pred in predecessor_state.getSuccessorRelatedValueList():
      predecessor_to_state_dict[pred] = frozenset(pred.getCompletedStateList())

    root_dict = {}
    # classify movements according to Root Applied Rules so we can look at
    # them closure per closure
    for movement in non_delivered_movement_list:
      root_dict.setdefault(movement.getRootAppliedRule(), []).append(movement)

    result = []
    # for each root applied rule, get buildable Movements
    for root_rule, movement_list in root_dict.iteritems():
      result.extend(self._filterBuildableInSameClosure(movement_list,
                                          predecessor_to_state_dict))
    return result



  def _filterBuildableInSameClosure(self, movement_list, predecessor_to_state_dict):
    """
    Return the buildable movements in movement_list.

    It is about finding in the tree the movements that have causalities in
    predecessor_to_state_dict keys.

    Three main steps to find those movements, executed in least expensive
    to most expensive order, hoping that step n allows us to return early
    without having to execute n+1:
      - look at ancestors of movement_list
      - query catalog for descendants of movement_list, hoping that
        it would be recent enough to list them all
      - manually walk descendants of movement_list in ZODB
    """
    buildable_list = []

    # track relations within movement_list if any
    # movement->(set of descendants in movement_list)
    descendant_dict = {}

    # contains a movement -> (dict of predecessors that we still havent met)
    # only contains the movements that have not proved to be unbuildable until
    # now.
    movement_looking_for_dict = {}

    def isBuiltAndCompleted(simulation, path):
678
      return simulation.getDeliveryValue() is not None and \
679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
          simulation.getSimulationState() in predecessor_to_state_dict[path]

    ### Step 1:
    ## Explore ancestors
    #

    for movement in movement_list:
      # set of predecessors
      looking_for = set(predecessor_to_state_dict)
      current = movement.getParentValue()

      maybeBuildable = True

      # visit all parents until Root Applied Rule
      while looking_for and maybeBuildable:
        portal_type = current.getPortalType()
        if portal_type == "Simulation Movement":
          # exploring ancestors is a great way to initialize
          # descendant_dict, while we're at it.
          if current in movement_looking_for_dict:
            descendant_dict.setdefault(current, set()).add(movement)

          path = current.getCausalityValue()
          if path in looking_for:
            looking_for.remove(path)
            if not isBuiltAndCompleted(current, path):
              maybeBuildable = False

        elif portal_type != "Applied Rule":
          break
        # XXX or maybe directly go up by two levels?
        current = current.getParentValue()

      if maybeBuildable:
        if not looking_for:
          buildable_list.append(movement)
        else:
          movement_looking_for_dict[movement] = looking_for

    # Maybe we're lucky, and we've found all predecessors of all
    # movements.
    # We can thus return the buildable ones and we're done.
    if not movement_looking_for_dict:
      return buildable_list

    def updateDescendantDictAndReturnSmallestAncestorSet():
      """
      Remove from descendant_dict the movements that are not
      buildable.

      Returns the smallest set of ancestors A that satisfies:
        - A <= movement_looking_for_dict.keys()
        - descendants(A) = descendants(movement_looking_for_dict.keys())
      (a.k.a. for any ai, aj in A, ai is not a descendant or an ancestor
       of aj)
      """
      movement_to_query = set(movement_looking_for_dict)

      if descendant_dict:
        # remove movements that have been eliminated
        for k, v in descendant_dict.items():
          if k not in movement_looking_for_dict:
            del descendant_dict[k]
          else:
            v.intersection_update(movement_looking_for_dict)

            movement_to_query.difference_update(v)
      return movement_to_query

    movement_to_query = updateDescendantDictAndReturnSmallestAncestorSet()

    ### Step 2:
    ## Try catalog to find descendant movements, knowing
    # that it can be incomplete

    class treeNode(dict):
      """
      Used to cache accesses to ZODB objects.
      The idea is to put in visited_movement_dict the objects we've already
      loaded from ZODB to avoid loading them again.

      - self represents a single ZODB container c
      - self.visited_movement_dict contains an id->(ZODB obj) cache for
        subobjects of c
      - self[id] contains the treeNode representing c[id]
      """
      def __init__(self):
        dict.__init__(self)
        self.visited_movement_dict = dict()

    path_tree = treeNode()

    def updateTree(simulation_movement, path):
      """
      Mark simulation_movement as visited in the Tree

      Returns the list of movements in movement_looking_for_dict that
      are ancestors of simulation_movement
      """
      traversed = []

      tree_node = path_tree
      movement_path = simulation_movement.getPhysicalPath()
      simulation_movement_id = movement_path[-1]
      # find container
      for path_id in movement_path[:-1]:
        # mark traversed movements that are in movement_looking_for_dict
        mvmt, ignored = tree_node.visited_movement_dict.get(path_id, (None, None))
        if mvmt is not None and mvmt in movement_looking_for_dict:
          traversed.append(mvmt)

        tree_node = tree_node.setdefault(path_id, treeNode())

      # and mark the object as visited
      tree_node.visited_movement_dict[simulation_movement_id] = (simulation_movement, path)
      return traversed

    # initialization
    for movement in movement_looking_for_dict:
      updateTree(movement, None)

    portal_catalog = self.getPortalObject().portal_catalog
    catalog_simulation_movement_list = portal_catalog(
      portal_type='Simulation Movement',
      causality_uid=[p.getUid() for p in predecessor_to_state_dict],
      path=['%s/%%' % m.getPath() for m in movement_to_query])

    unbuildable = set()
    for movement in catalog_simulation_movement_list:
      path = movement.getCausalityValue()
      traversed = updateTree(movement, path)
      if not isBuiltAndCompleted(movement, path):
        unbuildable.update(traversed)

    if len(unbuildable) == len(movement_looking_for_dict):
      # the sets are equals
      return buildable_list

    for m in unbuildable:
      del movement_looking_for_dict[m]

    ### Step 3:
    ## We had no luck, we have to explore descendant movements in ZODB
    #

    def findInTree(movement):
      # descend in the tree to find self:
      tree_node = path_tree
      for path_id in movement.getPhysicalPath():
        tree_node = tree_node.get(path_id, treeNode())
      return tree_node

    def descendantGenerator(document, tree_node, path_set_to_check):
      """
      generator yielding Simulation Movement descendants of document.
      It does _not_ explore the whole subtree if iteration is stopped.

      It uses the tree we built previously to avoid loading again ZODB
      objects that we already loaded during catalog querying

      path_set_to_check contains a set of Business Paths that we are
      interested in. A branch is only explored if this set is not
      empty; a movement is only yielded if its causality value is in this set
      """
      object_id_list = document.objectIds()
      for id in object_id_list:
        if id not in tree_node.visited_movement_dict:
          # we had not visited it in step #2
          subdocument = document._getOb(id)
          if subdocument.getPortalType() == "Simulation Movement":
            path = subdocument.getCausalityValue()
            t = (subdocument, path)
            tree_node.visited_movement_dict[id] = t
            if path in path_set_to_check:
              yield t
          else:
            # it must be an Applied Rule
            subtree = tree_node.get(id, treeNode())
            for d in descendantGenerator(subdocument,
                                         subtree,
                                         path_set_to_check):
              yield d

      for id, t in tree_node.visited_movement_dict.iteritems():
        subdocument, path = t
        if path is None:
          # happens for movement in movement_list
          continue
        to_check = path_set_to_check
        # do we need to change/copy the set?
        if path in to_check:
          if len(to_check) == 1:
            # no more paths to check in this branch
            continue
          to_check = to_check.copy()
          to_check.remove(path)
        subtree = tree_node.get(id, treeNode())
        for d in descendantGenerator(subdocument, subtree, to_check):
          yield d


    for movement in updateDescendantDictAndReturnSmallestAncestorSet():
      tree_node = findInTree(movement)
      remaining_path_set = movement_looking_for_dict[movement]
      # find descendants
      for descendant, path in descendantGenerator(self,
                                                  tree_node,
                                                  remaining_path_set):
        if not isBuiltAndCompleted(descendant, path):
          break
      else:
        buildable_list.append(movement)
        buildable_list.extend(descendant_dict.get(movement, []))

    return buildable_list