BusinessProcess.py 30.8 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 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

from AccessControl import ClassSecurityInfo
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
32
from Products.ERP5Type import Permissions, PropertySheet, interfaces
33 34
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5.Document.Path import Path
35
from Products.ERP5.ExplanationCache import _getExplanationCache, _getBusinessPathClosure
36

37 38
import zope.interface

39 40 41 42 43
class BusinessProcess(Path, XMLObject):
  """
    The BusinessProcess class is a container class which is used
    to describe business processes in the area of trade, payroll
    and production.
44 45 46

    TODO:
      - finish interface implementation
47 48
    RENAME:
      getPathValueList -> getBusinessPathValueList
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  """
  meta_type = 'ERP5 Business Process'
  portal_type = 'Business Process'

  # 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
65
                    , PropertySheet.BusinessProcess
66 67
                    )

68 69 70 71
  # Declarative interfaces
  zope.interface.implements(interfaces.IBusinessProcess,
                            interfaces.IArrowBase)

72 73 74 75 76 77 78 79 80 81 82
  # IBusinessPathProcess implementation
  security.declareProtected(Permissions.AccessContentsInformation, 'getBusinessPathValueList')
  def getBusinessPathValueList(self, trade_phase=None, context=None,
                               predecessor=None, successor=None, **kw):
    """Returns all Path of the current BusinessProcess which
    are matching the given trade_phase and the optional context.

    trade_phase -- filter by trade phase

    context -- a context to test each Business Path on
               and filter out Business Path which do not match
83

84
    predecessor -- filter by trade state predecessor
85

86
    successor -- filter by trade state successor
87

88
    **kw -- same arguments as those passed to searchValues / contentValues
89
    """
90
    if trade_phase is None:
91
      trade_phase = set()
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
92
    elif not isinstance(trade_phase, (list, tuple)):
93 94 95
      trade_phase = set((trade_phase,))
    else:
      trade_phase = set(trade_phase)
96
    result = []
97 98 99 100 101
    if kw.get('portal_type', None) is None:
      kw['portal_type'] = self.getPortalBusinessPathTypeList()
    if kw.get('sort_on', None) is None:
      kw['sort_on'] = 'int_index'
    original_business_path_list = self.objectValues(**kw), # Why Object Values ??? XXX-JPS
102
    if len(trade_phase) == 0:
103 104
      return original_business_path_list # If not trade_phase is specified, return all Business Path
    # Separate the selection of business paths into two steps
105 106 107
    # for easier debugging.
    # First, collect business paths which can be applicable to a given context.
    business_path_list = []
108 109 110 111 112
    for business_path in original_business_path_list:
      if predecessor is not None and business_path.getPredecessor() != predecessor:
        break # Filter our business path which predecessor does not match
      if successor is not None and business_path.getSuccessor() != successor:
        break # Filter our business path which predecessor does not match
113 114 115 116 117 118 119 120 121
      if trade_phase.intersection(business_path.getTradePhaseList()):
        business_path_list.append(business_path)
    # Then, filter business paths by Predicate API.
    # FIXME: Ideally, we should use the Domain Tool to search business paths,
    # and avoid using the low level Predicate API. But the Domain Tool does
    # support the condition above without scripting?
    for business_path in business_path_list:
      if business_path.test(context):
        result.append(business_path)
122 123
    return result

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
  def isBusinessPathCompleted(self, explanation, business_path):
    """Returns True if given Business Path document
    is completed in the context of provided explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    business_path -- a Business Path document
    """
    # Return False if Business Path is not completed
    if not business_path.isCompleted(explanation):
      return False
    predecessor_state = business_path.getPredecessor()
    if not predecessor_state:
      # This is a root business path, no predecessor
      # so no need to do any recursion
      return True
    if self.isTradeStateCompleted(explanation, predecessor_state):
      # If predecessor state is globally completed for the 
      # given explanation, return True
      # Please note that this is a specific case for a Business Process
      # built using asUnionBusinessProcess. In such business process
      # a business path may be completed even if its predecessor state
      # is not
      return True
    # Build the closure business process which only includes those business 
    # path wich are directly related to the current business path but DO NOT 
    # narrow down the explanation else we might narrow down so much that
    # it becomes an empty set
    closure_process = _getBusinessPathClosure(explanation, business_path)
    return closure_process.isTradeStateCompleted(explanation, predecessor_state)

  def isBusinessPathPartiallyCompleted(self, explanation, business_path):
    """Returns True if given Business Path document
    is partially completed in the context of provided explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    business_path -- a Business Path document
    """
    # Return False if Business Path is not partially completed
    if not business_path.isPartiallyCompleted(explanation):
      return False
    predecessor_state = business_path.getPredecessor()
    if not predecessor_state:
      # This is a root business path, no predecessor
      # so no need to do any recursion
      return True
    if self.isTradeStatePartiallyCompleted(explanation, predecessor_state):
      # If predecessor state is globally partially completed for the 
      # given explanation, return True
      # Please note that this is a specific case for a Business Process
      # built using asUnionBusinessProcess. In such business process
      # a business path may be partially completed even if its predecessor
      # state is not
      return True
    # Build the closure business process which only includes those business 
    # path wich are directly related to the current business path but DO NOT 
    # narrow down the explanation else we might narrow down so much that
    # it becomes an empty set
    closure_process = _getBusinessPathClosure(explanation, business_path)
    return closure_process.isTradeStatePartiallyCompleted(explanation, 
                                                           predecessor_state)

  def getExpectedBusinessPathCompletionDate(explanation, business_path, 
                                                       delay_mode=None):
    """Returns the expected completion date of given Business Path document
    in the context of provided explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    business_path -- a Business Path document

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
    """
    closure_process = _getBusinessPathClosure(explanation, business_path)
    # XXX use explanatoin cache to optimize
    predecessor = business_path.getPredecessor()
    if closure_process.isTradeStateRootState(predecessor):
      return business_path.getCompletionDate(explanation)

    reference_date = closure_process.getExpectedTradeStateCompletionDate(explanation, predecessor)

    return reference_date + wait_time + lead_time
    
212

213 214 215 216
  def getExpectedBusinessPathStartAndStopDate(explanation, business_path,
                                                         delay_mode=None):
    """Returns the expected start and stop dates of given Business Path
    document in the context of provided explanation.
217

218 219
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
220

221
    business_path -- a Business Path document
222

223 224
    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
225
    """
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
    closure_process = _getBusinessPathClosure(explanation, business_path)
    # XXX use explanatoin cache to optimize
    trade_date = self.getTradeDate()
    if trade_date is not None:
      reference_date = closure_process.gettExpectedTradePhaseCompletionDate(explanation, trade_date)
    else:
      predecessor = business_path.getPredecessor() # XXX-JPS they should all have
      reference_date = closure_process.getExpectedTradeStateCompletionDate(explanation, predecessor)

    start_date = reference_date + wait_time
    stop_date = start_date + lead_time # XXX-JPS use appropriate values
    
    return start_date, stop_date

  # IBuildableBusinessPathProcess implementation
  def getBuildableBusinessPathValueList(self, explanation):
    """Returns the list of Business Path which are buildable
    by taking into account trade state dependencies between
    Business Path.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
248
    """
249 250 251 252 253 254 255 256 257 258
    result = []
    for business_path in self.getBusinessPathValueList():
      if self.isBusinessPathBuildable(explanation, business_path):
        result.append(business_path)
    return result

  def getPartiallyBuildableBusinessPathValueList(self, explanation):
    """Returns the list of Business Path which are partially buildable
    by taking into account trade state dependencies between
    Business Path.
259

260 261
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
262
    """
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
    result = []
    for business_path in self.getBusinessPathValueList():
      if self.isBusinessPathPartiallyBuildable(explanation, business_path):
        result.append(business_path)
    return result

  def isBusinessPathBuildable(self, explanation, business_path):
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is completed.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    business_path -- a Business Path document
    """
    # If everything is delivered, no need to build
    if business_path.isDelivered(explanation):
      return False
    # We must take the closure cause only way to combine business process
    closure_process = _getBusinessPathClosure(explanation, business_path)
    predecessor = business_path.getPredecessor()
    return closure_process.isTradeStateCompleted(predecessor)

  def isBusinessPathPartiallyBuildable(self, explanation, business_path):
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is partially completed.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    business_path -- a Business Path document
    """
    # If everything is delivered, no need to build
    if business_path.isDelivered(explanation):
      return False
    # We must take the closure cause only way to combine business process
    closure_process = _getBusinessPathClosure(explanation, business_path)
    predecessor = business_path.getPredecessor()
    return closure_process.isTradeStatePartiallyCompleted(predecessor)

  # ITradeStateProcess implementation
  def getTradeStateList():
    """Returns list of all trade_state of this Business Process
    by looking at successor and predecessor values of contained
    Business Path.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
    """
    result = set()
    for business_path in self.getBusinessPathValueList():
      result.add(business_path.getPredecessor())
      result.add(business_path.getSuccessor())
    return result

  def getSuccessorTradeStateList(explanation, trade_state):
    """Returns the list of successor states in the 
    context of given explanation. This list is built by looking
    at all successor of business path involved in given explanation
    and which predecessor is the given trade_phase.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_state -- a Trade State category
328
    """
329 330 331 332 333
    result = set()
    for business_path in self.getBusinessPathValueList():
      if business_path.getPredecessor() == trade_state:
        result.add(business_path.getSuccessor())
    return result
334

335 336 337 338 339 340 341 342 343 344
  def getPredecessorTradeStateList(explanation, trade_state):
    """Returns the list of predecessor states in the 
    context of given explanation. This list is built by looking
    at all predecessor of business path involved in given explanation
    and which sucessor is the given trade_phase.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_state -- a Trade State category
345
    """
346 347 348 349 350 351 352 353 354 355 356 357
    result = set()
    for business_path in self.getBusinessPathValueList():
      if business_path.getSuccessor() == trade_state:
        result.add(business_path.getPredecessor())
    return result

  def getCompletedTradeStateList(explanation):
    """Returns the list of Trade States which are completed
    in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
358
    """
359 360 361 362 363
    return filter(lambda x:self.isTradeStateCompleted(explanation, x), self.getTradeStateList())

  def getPartiallyCompletedTradeStateList(explanation):
    """Returns the list of Trade States which are partially 
    completed in the context of given explanation.
364

365 366
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
367
    """
368 369 370 371 372 373 374 375 376
    return filter(lambda x:self.isTradeStatePartiallyCompleted(explanation, x), self.getTradeStateList())

  def getLatestCompletedTradeStateList(explanation):
    """Returns the list of completed trade states which predecessor
    states are completed and for which no successor state 
    is completed in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
377
    """
378 379 380 381 382
    result = set()
    for state in self.getCompletedTradeStateValue(explanation):
      for business_path in state.getPredecessorRelatedValueList():
        if not self.isBusinessPathCompleted(explanation, business_path):
          result.add(state)
383 384
    return result

385 386 387 388 389 390 391
  def getLatestPartiallyCompletedTradeStateList(explanation):
    """Returns the list of completed trade states which predecessor
    states are completed and for which no successor state 
    is partially completed in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
392
    """
393 394 395 396 397
    result = set()
    for state in self.getCompletedTradeStateValue(explanation):
      for business_path in state.getPredecessorRelatedValueList():
        if not self.isBusinessPathPartiallyCompleted(explanation, business_path):
          result.add(state)
398 399
    return result

400 401 402 403 404 405 406 407 408
  def isTradeStateCompleted(explanation, trade_state):
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is completed
    in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_state -- a Trade State category
409
    """
410 411 412 413
    for business_path in self.getBusinessPathValueList(successor=trade_state):
      if not closure_process.isBusinessPathCompleted(explanation, business_path):
        return False
    return True      
414

415 416 417 418
  def isTradeStatePartiallyCompleted(explanation, trade_state):
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is partially completed
    in the context of given explanation.
419

420 421
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
422

423
    trade_state -- a Trade State category
424
    """
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
    for business_path in self.getBusinessPathValueList(successor=trade_state):
      if not self.isBusinessPathPartiallyCompleted(explanation, business_path):
        return False
    return True      

  def getExpectedTradeStateCompletionDate(explanation, trade_state,
                                                         delay_mode=None):
    """Returns the date at which the give trade state is expected
    to be completed in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_state -- a Trade State category

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
442
    """
443 444 445 446
    date_list = []
    for business_path in self.getBusinessPathValueList(successor=trade_state):
      date_list.append(self.getExpectedBusinessPathCompletionDate(explanation, business_path))
    return max(date_list) # XXX-JPS provide -infinite for...
447

448 449 450 451
  # ITradePhaseProcess implementation
  def getTradePhaseList():
    """Returns list of all trade_phase of this Business Process
    by looking at trade_phase values of contained Business Path.
452
    """
453 454 455 456 457 458 459 460 461 462 463
    result = set()
    for business_path in self.getBusinessPathValueList():
      result.union(business_path.getTradePhaseList())
    return result

  def getCompletedTradePhaseList(explanation):
    """Returns the list of Trade Phases which are completed
    in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
464
    """
465
    return filter(lambda x:self.isTradePhaseCompleted(explanation, x), self.getTradePhaseList())
466
    
467 468 469
  def getPartiallyCompletedTradePhaseList(explanation):
    """Returns the list of Trade Phases which are partially completed
    in the context of given explanation. 
470

471 472
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
473
    """
474
    return filter(lambda x:self.isTradePhasePartiallyCompleted(explanation, x), self.getTradePhaseList())
475

476 477 478 479 480 481 482 483
  def isTradePhaseCompleted(explanation, trade_phase):
    """Returns True all business path with given trade_phase
    applicable to given explanation are completed.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_phase -- a Trade Phase category
484
    """
485 486 487 488
    for business_path in self.getBusinessPathValueList(trade_phase=trade_phase):
      if not self.isBusinessPathCompleted(explanation, business_path):
        return False
    return True
489

490 491 492 493
  def isTradePhasePartiallyCompleted(explanation, trade_phase):
    """Returns True at least one business path with given trade_phase
    applicable to given explanation is partially completed
    or completed.
494

495 496
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
497

498 499 500 501 502 503
    trade_phase -- a Trade Phase category
    """
    for business_path in self.getBusinessPathValueList(trade_phase=trade_phase):
      if not self.isBusinessPathPartiallyCompleted(explanation, business_path):
        return False
    return True
504

505 506 507 508 509 510
  def getExpectedTradePhaseCompletionDate(explanation, trade_phase,
                                                       delay_mode=None):
    """Returns the date at which the give trade phase is expected
    to be completed in the context of given explanation, taking
    into account the graph of date constraints defined by business path
    and business states.
511

512 513 514 515
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree

    trade_phase -- a Trade Phase category
516

517 518
    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
519
    """
520 521 522 523
    date_list = []
    for business_path in self.getBusinessPathValueList(trade_phase=trade_phase):
      date_list.append(self.getExpectedTradePhaseCompletionDate(explanation, business_path, delay_mode=delay_mode))
    return max(date_list)
524

525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
  def getRemainingTradePhaseList(business_path, trade_phase_list=None):
    """Returns the list of remaining trade phases which to be achieved
    as part of a business process. This list is calculated by analysing 
    the graph of business path and trade states, starting from a given
    business path. The result if filtered by a list of trade phases. This
    method is useful mostly for production and MRP to manage a distributed
    supply and production chain.

    business_path -- a Business Path document

    trade_phase_list -- if provided, the result is filtered by it after
                        being collected - ???? useful ? XXX-JPS ?

    NOTE: explanation is not involved here because we consider here that
    self is the result of asUnionBusinessProcess and thus only contains
    applicable Business Path to a given simulation subtree. Since the list
    of remaining trade phases does not depend on exact values in the
    simulation, we did not include the explanation. However, this makes the
    API less uniform.
544
    """
545 546 547

    # XXXX REVIEWJPS

548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
    remaining_trade_phase_list = []
    for path in [x for x in self.objectValues(portal_type="Business Path") \
        if x.getPredecessorValue() == trade_state]:
      # XXX When no simulations related to path, what should path.isCompleted return?
      #     if True we don't have way to add remaining trade phases to new movement
      if not (path.getRelatedSimulationMovementValueList(explanation) and
              path.isCompleted(explanation)):
        remaining_trade_phase_list += path.getTradePhaseValueList()

      # collect to successor direction recursively
      state = path.getSuccessorValue()
      if state is not None:
        remaining_trade_phase_list.extend(
          self.getRemainingTradePhaseList(explanation, state, None))

    # filter just at once if given
    if trade_phase_list is not None:
      remaining_trade_phase_list = filter(
        lambda x : x.getLogicalPath() in trade_phase_list,
        remaining_trade_phase_list)

    return remaining_trade_phase_list

571 572 573 574 575 576 577
  # IBusinessProcess global API
  def isCompleted(explanation):
    """Returns True is all applicable Trade States and Trade Phases
    are completed in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
578
    """
579 580
    for state in self.getTradeStateList():
      if not state.isTradeStateCompleted(explanation):
581 582 583
        return False
    return True

584 585 586 587 588 589
  def getExpectedCompletionDate(explanation, delay_mode=None):
    """Returns the expected date at which all applicable Trade States and
    Trade Phases are completed in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
590
    """
591 592 593 594
  
  def isBuildable(explanation):
    """Returns True is one Business Path of this Business Process
    is buildable in the context of given explanation.
595

596 597
    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
598
    """
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    return len(self.getBuildableBusinessPathValueList(explanation)) != 0

  def isPartiallyBuildable(explanation):
    """Returns True is one Business Path of this Business Process
    is partially buildable in the context of given explanation.

    explanation -- an Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree
    """
    return len(self.getPartiallyBuildableBusinessPathValueList(explanation)) != 0

  def build(self, explanation):
    """
      Build whatever is buildable
    """
    for business_path in self.getBuildableBusinessPathValueList(explanation):
      business_path.build(explanation)


  # GARBAGE - XXXXXXXXXXXXXXXXXXXXXXX
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

  def getExpectedStateBeginningDate(self, explanation, trade_state, *args, **kwargs):
    """
      Returns the expected beginning date for this
      state based on the explanation.

      explanation -- the document
    """
    # Should be re-calculated?
    # XXX : what is the purpose of the two following lines ? comment it until there is
    # good answer
    if 'predecessor_date' in kwargs:
      del kwargs['predecessor_date']
    predecessor_list = [x for x in self.objectValues(portal_type="Business Path") \
        if x.getPredecessorValue() == trade_state]
    date_list = self._getExpectedDateList(explanation,
                                          predecessor_list,
                                          self._getExpectedBeginningDate,
                                          *args,
                                          **kwargs)
    if len(date_list) > 0:
      return min(date_list)

  def _getExpectedBeginningDate(self, path, *args, **kwargs):
    expected_date = path.getExpectedStartDate(*args, **kwargs)
    if expected_date is not None:
      return expected_date - path.getWaitTime()

  def _getExpectedDateList(self, explanation, path_list, path_method,
                           visited=None, *args, **kwargs):
    """
      getExpected(Beginning/Completion)Date are same structure
      expected date of each path should be returned.

      explanation -- the document
      path_list -- list of target business path
      path_method -- used to get expected date on each path
      visited -- only used to prevent infinite recursion internally
    """
    if visited is None:
      visited = []

    expected_date_list = []
    for path in path_list:
      # filter paths without path of root explanation
      if path not in visited or path.isDeliverable():
        expected_date = path_method(path, explanation, visited=visited, *args, **kwargs)
        if expected_date is not None:
          expected_date_list.append(expected_date)

    return expected_date_list

671 672 673 674 675 676 677 678 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
  def getRootExplanationPathValue(self): # XXX-JPS not in API
    """
      Returns a root path of this business process
    """
    path_list = self.objectValues(portal_type=self.getPortalBusinessPathTypeList())
    path_list = filter(lambda x: x.isDeliverable(), path_list)
    
    if len(path_list) > 1:
      raise Exception, "this business process has multi root paths"

    if len(path_list) == 1:
      return path_list[0]

  def getHeadPathValueList(self, trade_phase_list=None): # XXX-JPS not in API
    """
      Returns a list of head path(s) of this business process

      trade_phase_list -- used to filtering, means that discovering
                          a list of head path with the trade_phase_list
    """
    head_path_list = list()
    for state in self.getStateValueList():
      if len(state.getSuccessorRelatedValueList()) == 0:
        head_path_list += state.getPredecessorRelatedValueList()

    if trade_phase_list is not None:
      _set = set(trade_phase_list)
      _list = list()
      # start to discover a head path with the trade_phase_list from head path(s) of whole
      for path in head_path_list:
        _list += self._getHeadPathValueList(path, _set)
      head_path_list = map(lambda t: t[0], filter(lambda t: t != (None, None), _list))

    return head_path_list

  def _getHeadPathValueList(self, path, trade_phase_set):
    # if the path has target trade_phase, it is a head path.
    _set = set(path.getTradePhaseList())
    if _set & trade_phase_set:
      return [(path, _set & trade_phase_set)]

    node = path.getSuccessorValue()
    if node is None:
      return [(None, None)]

    _list = list()
    for next_path in node.getPredecessorRelatedValueList():
      _list += self._getHeadPathValueList(next_path, trade_phase_set)
    return _list

721 722 723
  def _getExpectedCompletionDate(self, path, *args, **kwargs):
    return path.getExpectedStopDate(*args, **kwargs)

724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

  # From Business Path
  def _getExplanationUidList(self, explanation):
    """
    Helper method to fetch really explanation related movements 
    """ # XXX-JPS this seems to be doing too much - why do you need many "explanations"
    explanation_uid_list = [explanation.getUid()]
    # XXX: getCausalityRelatedValueList is oversimplification, assumes
    #      that documents are sequenced like simulation movements, which
    #      is wrong
    if getattr(explanation, "getCausalityValueList", None) is None: 
      return explanation_uid_list
    for found_explanation in explanation.getCausalityRelatedValueList( # XXX-JPS this also seems exagerated, and breaking the APIs
        portal_type=self.getPortalDeliveryTypeList()) + \
        explanation.getCausalityValueList(): # Wrong if an item
      explanation_uid_list.extend(self._getExplanationUidList(
        found_explanation))
    return explanation_uid_list