BusinessProcess.py 36.7 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, _getBusinessLinkClosure
36
from Products.ERP5.MovementCollectionDiff import _getPropertyAndCategoryList
37

38 39
import zope.interface

40
class BusinessProcess(Path, XMLObject):
41 42
  """The BusinessProcess class is a container class which is used
  to describe business processes in the area of trade, payroll
43
  and production. Processes consists of a collection of Business Link
44 45 46
  which define an arrow between a 'predecessor' trade_state and a 
  'successor' trade_state, for a given trade_phase_list.

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  Core concepts in BusinessProcess are the notions of "explanation".
  Explanation represents the subtree of a simulation tree of all 
  simulation movements related to an applied rule, a delivery line,
  a delivery, etc.

  Example:
    portal_simulation/2/sm1/a1/sm2/a2/sm3
    portal_simulation/2/sm1/a1/sm2/a2/sm4

    explanation(portal_simulation/2/sm1/a1/sm2/a2) is
      portal_simulation/2/sm1/a1/sm2/a2/sm3
      portal_simulation/2/sm1/a1/sm2/a2/sm4
      portal_simulation/2/sm1/a1/sm2
      portal_simulation/2/sm1

  Business Process completion, dates, etc. are calculated
  always in the context of an explanation. Sometimes, 
64
  it is necessary to ignore certain business link to evaluate
65
  completion or completion dates. This is very true for Union 
66 67
  Business Processes. This is the concept of Business Link closure,
  ie. filtering out all Business Link which are not used in an explanation.
68

69 70 71 72 73 74 75 76 77 78
  TODO:
  - add support to prevent infinite loop. (but beware, this notion has changed
    with Union of Business Process, since the loop should be detected only
    as part of a given business process closure)
  - handle all properties of PaymentCondition in date calculation
  - review getRemainingTradePhaseList
  - optimize performance so that the completion dates are calculated
    only once in a transaction thanks to caching (which could be 
    handled in coordination with ExplanationCache infinite loop
    detection)
79 80 81 82 83
  - fine a better way to narrow down paremeters to copy without
    using a rule parameter
  - should _getPropertyAndCategoryList remain a private method or
    become part of IMovement ?
  - add security declarations
84 85
  - why are we using objectValues in some places ?
  - add a property to rules in order to determine whether dates
86
    are provided by the rule or by business link / trade model path. This is an extension
87 88 89 90
    of the idea that root applied rules provide date information.
  - use explanation cache more to optimize speed
  - DateTime must be extended in ERP5 to support  +infinite and -infinite 
    like floating points do
91
  - support conversions in trade model path
92 93

  RENAMED:
94
    getPathValueList -> getBusinessLinkValueList
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
  """
  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
111
                    , PropertySheet.BusinessProcess
112 113
                    )

114 115 116 117
  # Declarative interfaces
  zope.interface.implements(interfaces.IBusinessProcess,
                            interfaces.IArrowBase)

118 119 120 121 122 123 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
  # ITradeModelPathProcess implementation
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradeModelPathValueList')
  def getTradeModelPathValueList(self, trade_phase=None, context=None, **kw):
    """Returns all Trade Model Path of the current Business Process which
    are matching the given trade_phase and the optional context.

    trade_phase -- filter by trade phase

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

    **kw -- same arguments as those passed to searchValues / contentValues
    """
    if trade_phase is None:
      trade_phase = set()
    elif not isinstance(trade_phase, (list, tuple)):
      trade_phase = set((trade_phase,))
    else:
      trade_phase = set(trade_phase)
    result = []
    if kw.get('portal_type', None) is None:
      kw['portal_type'] = self.getPortalTradeModelPathTypeList()
    if kw.get('sort_on', None) is None:
      kw['sort_on'] = 'int_index'
    original_path_list = self.objectValues(**kw) # Why Object Values ??? XXX-JPS
    # Separate the selection of trade model paths into two steps
    # for easier debugging.
    # First, collect trade model paths which can be applicable to a given context.
    path_list = []
    for path in original_path_list:
      accept_path = True
      if trade_phase is not None and not trade_phase.intersection(path.getTradePhaseList()):
        accept_path = False # Filter our business path which trade phase does not match
      if accept_path:
        path_list.append(path)
    # Then, filter trade model 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 path in path_list:
      if path.test(context):
        result.append(path)
    return result

  security.declareProtected(Permissions.AccessContentsInformation, 'getExpectedTradeModelPathStartAndStopDate')
  def getExpectedTradeModelPathStartAndStopDate(self, explanation, trade_model_path,
                                                      delay_mode=None):
    """Returns the expected start and stop dates of given Trade Model Path
    document in the context of provided explanation.

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

    trade_model_path -- a Trade Model Path document

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
    """
    if explanation.getPortalType() != 'Applied Rule':
      raise TypeError('explanation must be an Applied Rule')

    if explanation.getParentValue().getPortalType() == 'Simulation Tool':
      raise ValueError('explanation must not be a Root Applied Rule')

    trade_date = trade_model_path.getTradeDate()
    if not trade_date:
      raise ValueError('a trade_date must be defined on every Trade Model Path')

    reference_date_method_id = trade_model_path.getReferenceDateMethodId()
    if not reference_date_method_id:
      raise ValueError('a reference date method must be defined on every Trade Model Path')

    explanation_cache = _getExplanationCache(self, explanation)
    reference_date = explanation_cache.getReferenceDate(self, trade_date, reference_date_method_id)

    # Computer start_date and stop_date (XXX-JPS this could be cached and accelerated)
    start_date = reference_date + trade_model_path.getPaymentTerm() # XXX-JPS Until better naming
    if delay_mode == 'min':
      delay = trade_model_path.getMinDelay()
    elif delay_mode == 'max':
      delay = trade_model_path.getMaxDelay()
    else:
      delay = (business_link.getMaxDelay() + trade_model_path.getMinDelay()) / 2.0
    stop_date = start_date + delay
        
    return start_date, stop_date

205 206 207
  # IBusinessLinkProcess implementation
  security.declareProtected(Permissions.AccessContentsInformation, 'getBusinessLinkValueList')
  def getBusinessLinkValueList(self, trade_phase=None, context=None,
208
                               predecessor=None, successor=None, **kw):
209
    """Returns all Business Links of the current BusinessProcess which
210 211 212 213
    are matching the given trade_phase and the optional context.

    trade_phase -- filter by trade phase

214 215
    context -- a context to test each Business Link on
               and filter out Business Link which do not match
216

217
    predecessor -- filter by trade state predecessor
218

219
    successor -- filter by trade state successor
220

221
    **kw -- same arguments as those passed to searchValues / contentValues
222
    """
223
    if trade_phase is None:
224
      trade_phase = set()
Kazuhiko Shiozaki's avatar
Kazuhiko Shiozaki committed
225
    elif not isinstance(trade_phase, (list, tuple)):
226 227 228
      trade_phase = set((trade_phase,))
    else:
      trade_phase = set(trade_phase)
229
    result = []
230
    if kw.get('portal_type', None) is None:
231
      kw['portal_type'] = self.getPortalBusinessLinkTypeList()
232 233
    if kw.get('sort_on', None) is None:
      kw['sort_on'] = 'int_index'
234
    original_business_link_list = self.objectValues(**kw) # Why Object Values ??? XXX-JPS
235
    # Separate the selection of business links into two steps
236
    # for easier debugging.
237
    # First, collect business links which can be applicable to a given context.
238 239
    business_link_list = []
    for business_link in original_business_link_list:
240
      accept_link = True
241
      if predecessor is not None and business_link.getPredecessor() != predecessor:
242
        accept_link = False # Filter our business link which predecessor does not match
243
      if successor is not None and business_link.getSuccessor() != successor:
244
        accept_link = False # Filter our business link which predecessor does not match
245
      if trade_phase is not None and not trade_phase.intersection(business_link.getTradePhaseList()):
246 247
        accept_link = False # Filter our business link which trade phase does not match
      if accept_link:
248
        business_link_list.append(business_link)
249 250
    # Then, filter business links by Predicate API.
    # FIXME: Ideally, we should use the Domain Tool to search business links,
251 252
    # and avoid using the low level Predicate API. But the Domain Tool does
    # support the condition above without scripting?
253 254 255
    for business_link in business_link_list:
      if business_link.test(context):
        result.append(business_link)
256 257
    return result

258
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkCompleted')
259 260
  def isBusinessLinkCompleted(self, explanation, business_link):
    """Returns True if given Business Link document
261 262
    is completed in the context of provided explanation.

263 264
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
265

266
    business_link -- a Business Link document
267
    """
268 269
    # Return False if Business Link is not completed
    if not business_link.isCompleted(explanation):
270
      return False
271
    predecessor_state = business_link.getPredecessor()
272
    if not predecessor_state:
273
      # This is a root business links, no predecessor
274 275 276 277 278 279 280
      # 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
281
      # a business link may be completed even if its predecessor state
282 283 284
      # is not
      return True
    # Build the closure business process which only includes those business 
285
    # links wich are directly related to the current business link but DO NOT 
286 287
    # narrow down the explanation else we might narrow down so much that
    # it becomes an empty set
288
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
289 290
    return closure_process.isTradeStateCompleted(explanation, predecessor_state)

291
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkPartiallyCompleted')
292 293
  def isBusinessLinkPartiallyCompleted(self, explanation, business_link):
    """Returns True if given Business Link document
294 295
    is partially completed in the context of provided explanation.

296 297
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
298

299
    business_link -- a Business Link document
300
    """
301 302
    # Return False if Business Link is not partially completed
    if not business_link.isPartiallyCompleted(explanation):
303
      return False
304
    predecessor_state = business_link.getPredecessor()
305
    if not predecessor_state:
306
      # This is a root business link, no predecessor
307 308 309 310 311 312 313
      # 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
314
      # a business link may be partially completed even if its predecessor
315 316 317
      # state is not
      return True
    # Build the closure business process which only includes those business 
318
    # links wich are directly related to the current business link but DO NOT 
319 320
    # narrow down the explanation else we might narrow down so much that
    # it becomes an empty set
321
    closure_process = _getBusinessLinkClosure(explanation, business_link)
322 323 324
    return closure_process.isTradeStatePartiallyCompleted(explanation, 
                                                           predecessor_state)

325
  # IBuildableBusinessLinkProcess implementation
326
  security.declareProtected(Permissions.AccessContentsInformation, 'getBuildableBusinessLinkValueList')
327 328
  def getBuildableBusinessLinkValueList(self, explanation):
    """Returns the list of Business Link which are buildable
329
    by taking into account trade state dependencies between
330
    Business Link.
331

332 333
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
334
    """
335
    result = []
336 337 338
    for business_link in self.getBusinessLinkValueList():
      if self.isBusinessLinkBuildable(explanation, business_link):
        result.append(business_link)
339 340
    return result

341
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyBuildableBusinessLinkValueList')
342 343
  def getPartiallyBuildableBusinessLinkValueList(self, explanation):
    """Returns the list of Business Link which are partially buildable
344
    by taking into account trade state dependencies between
345
    Business Link.
346

347 348
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
349
    """
350
    result = []
351 352 353
    for business_link in self.getBusinessLinkValueList():
      if self.isBusinessLinkPartiallyBuildable(explanation, business_link):
        result.append(business_link)
354 355
    return result

356
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkBuildable')
357
  def isBusinessLinkBuildable(self, explanation, business_link):
358 359 360
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is completed.

361 362
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
363

364
    business_link -- a Business Link document
365 366
    """
    # If everything is delivered, no need to build
367
    if business_link.isDelivered(explanation):
368 369
      return False
    # We must take the closure cause only way to combine business process
370 371
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
    predecessor = business_link.getPredecessor()
372 373
    return closure_process.isTradeStateCompleted(predecessor)

374
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkPartiallyBuildable')
375
  def isBusinessLinkPartiallyBuildable(self, explanation, business_link):
376 377 378
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is partially completed.

379 380
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
381

382
    business_link -- a Business Link document
383 384
    """
    # If everything is delivered, no need to build
385
    if business_link.isDelivered(explanation):
386 387
      return False
    # We must take the closure cause only way to combine business process
388 389
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
    predecessor = business_link.getPredecessor()
390 391 392
    return closure_process.isTradeStatePartiallyCompleted(predecessor)

  # ITradeStateProcess implementation
393
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradeStateList')
394
  def getTradeStateList(self):
395 396
    """Returns list of all trade_state of this Business Process
    by looking at successor and predecessor values of contained
397
    Business Link.
398

399 400
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
401 402
    """
    result = set()
403 404 405
    for business_link in self.getBusinessLinkValueList():
      result.add(business_link.getPredecessor())
      result.add(business_link.getSuccessor())
406 407
    return result

408
  security.declareProtected(Permissions.AccessContentsInformation, 'isInitialTradeState')
409 410
  def isInitialTradeState(self, trade_state):
    """Returns True if given 'trade_state' has no successor related
411
    Business Link.
412 413 414

    trade_state -- a Trade State category
    """
415
    return len(self.getBusinessLinkValueList(successor=trade_state)) == 0
416

417
  security.declareProtected(Permissions.AccessContentsInformation, 'isFinalTradeState')
418 419
  def isFinalTradeState(self, trade_state):
    """Returns True if given 'trade_state' has no predecessor related
420
    Business Link.
421 422 423

    trade_state -- a Trade State category
    """
424
    return len(self.getBusinessLinkValueList(predecessor=trade_state)) == 0
425

426
  security.declareProtected(Permissions.AccessContentsInformation, 'getSuccessorTradeStateList')
427
  def getSuccessorTradeStateList(self, explanation, trade_state):
428 429
    """Returns the list of successor states in the 
    context of given explanation. This list is built by looking
430
    at all successor of business link involved in given explanation
431 432
    and which predecessor is the given trade_phase.

433 434
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
435 436

    trade_state -- a Trade State category
437
    """
438
    result = set()
439 440 441
    for business_link in self.getBusinessLinkValueList():
      if business_link.getPredecessor() == trade_state:
        result.add(business_link.getSuccessor())
442
    return result
443

444
  security.declareProtected(Permissions.AccessContentsInformation, 'getPredecessorTradeStateList')
445
  def getPredecessorTradeStateList(self, explanation, trade_state):
446 447
    """Returns the list of predecessor states in the 
    context of given explanation. This list is built by looking
448
    at all predecessor of business link involved in given explanation
449 450
    and which sucessor is the given trade_phase.

451 452
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
453 454

    trade_state -- a Trade State category
455
    """
456
    result = set()
457 458 459
    for business_link in self.getBusinessLinkValueList():
      if business_link.getSuccessor() == trade_state:
        result.add(business_link.getPredecessor())
460 461
    return result

462
  security.declareProtected(Permissions.AccessContentsInformation, 'getCompletedTradeStateList')
463
  def getCompletedTradeStateList(self, explanation):
464 465 466
    """Returns the list of Trade States which are completed
    in the context of given explanation.

467 468
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
469
    """
470 471
    return filter(lambda x:self.isTradeStateCompleted(explanation, x), self.getTradeStateList())

472
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyCompletedTradeStateList')
473
  def getPartiallyCompletedTradeStateList(self, explanation):
474 475
    """Returns the list of Trade States which are partially 
    completed in the context of given explanation.
476

477 478
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
479
    """
480 481
    return filter(lambda x:self.isTradeStatePartiallyCompleted(explanation, x), self.getTradeStateList())

482
  security.declareProtected(Permissions.AccessContentsInformation, 'getLatestCompletedTradeStateList')
483
  def getLatestCompletedTradeStateList(self, explanation):
484 485 486 487
    """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.

488 489
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
490
    """
491 492
    result = set()
    for state in self.getCompletedTradeStateValue(explanation):
493 494
      for business_link in state.getPredecessorRelatedValueList():
        if not self.isBusinessLinkCompleted(explanation, business_link):
495
          result.add(state)
496 497
    return result

498
  security.declareProtected(Permissions.AccessContentsInformation, 'getLatestPartiallyCompletedTradeStateList')
499
  def getLatestPartiallyCompletedTradeStateList(self, explanation):
500 501 502 503
    """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.

504 505
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
506
    """
507 508
    result = set()
    for state in self.getCompletedTradeStateValue(explanation):
509 510
      for business_link in state.getPredecessorRelatedValueList():
        if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
511
          result.add(state)
512 513
    return result

514
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradeStateCompleted')
515
  def isTradeStateCompleted(self, explanation, trade_state):
516 517 518 519
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is completed
    in the context of given explanation.

520 521
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
522 523

    trade_state -- a Trade State category
524
    """
525 526
    for business_link in self.getBusinessLinkValueList(successor=trade_state):
      if not closure_process.isBusinessLinkCompleted(explanation, business_link):
527 528
        return False
    return True      
529

530
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradeStatePartiallyCompleted')
531
  def isTradeStatePartiallyCompleted(self, explanation, trade_state):
532 533 534
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is partially completed
    in the context of given explanation.
535

536 537
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
538

539
    trade_state -- a Trade State category
540
    """
541 542
    for business_link in self.getBusinessLinkValueList(successor=trade_state):
      if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
543 544 545 546
        return False
    return True      

  # ITradePhaseProcess implementation
547
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradePhaseList')
548
  def getTradePhaseList(self):
549
    """Returns list of all trade_phase of this Business Process
550
    by looking at trade_phase values of contained Business Link.
551
    """
552
    result = set()
553 554
    for business_link in self.getBusinessLinkValueList():
      result = result.union(business_link.getTradePhaseList())
555 556
    return result

557
  security.declareProtected(Permissions.AccessContentsInformation, 'getCompletedTradePhaseList')
558
  def getCompletedTradePhaseList(self, explanation):
559 560 561
    """Returns the list of Trade Phases which are completed
    in the context of given explanation.

562 563
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
564
    """
565
    return filter(lambda x:self.isTradePhaseCompleted(explanation, x), self.getTradePhaseList())
566
    
567
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyCompletedTradePhaseList')
568
  def getPartiallyCompletedTradePhaseList(self, explanation):
569 570
    """Returns the list of Trade Phases which are partially completed
    in the context of given explanation. 
571

572 573
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
574
    """
575
    return filter(lambda x:self.isTradePhasePartiallyCompleted(explanation, x), self.getTradePhaseList())
576

577
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradePhaseCompleted')
578
  def isTradePhaseCompleted(self, explanation, trade_phase):
579
    """Returns True all business link with given trade_phase
580 581
    applicable to given explanation are completed.

582 583
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
584 585

    trade_phase -- a Trade Phase category
586
    """
587 588
    for business_link in self.getBusinessLinkValueList(trade_phase=trade_phase):
      if not self.isBusinessLinkCompleted(explanation, business_link):
589 590
        return False
    return True
591

592
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradePhasePartiallyCompleted')
593
  def isTradePhasePartiallyCompleted(self, explanation, trade_phase):
594
    """Returns True at least one business link with given trade_phase
595 596
    applicable to given explanation is partially completed
    or completed.
597

598 599
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
600

601 602
    trade_phase -- a Trade Phase category
    """
603 604
    for business_link in self.getBusinessLinkValueList(trade_phase=trade_phase):
      if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
605 606
        return False
    return True
607

608
  security.declareProtected(Permissions.AccessContentsInformation, 'getRemainingTradePhaseList')
609
  def getRemainingTradePhaseList(self, business_link, trade_phase_list=None):
610 611
    """Returns the list of remaining trade phases which to be achieved
    as part of a business process. This list is calculated by analysing 
612 613
    the graph of business link and trade states, starting from a given
    business link. The result if filtered by a list of trade phases. This
614 615 616
    method is useful mostly for production and MRP to manage a distributed
    supply and production chain.

617
    business_link -- a Business Link document
618 619

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

622 623
    NOTE: this code has not been reviewed and needs review

624 625
    NOTE: explanation is not involved here because we consider here that
    self is the result of asUnionBusinessProcess and thus only contains
626
    applicable Business Link to a given simulation subtree. Since the list
627 628 629
    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.
630 631
    """
    remaining_trade_phase_list = []
632
    for link in [x for x in self.objectValues(portal_type="Business Link") \
633
        if x.getPredecessorValue() == trade_state]:
634
      # XXX When no simulations related to link, what should link.isCompleted return?
635
      #     if True we don't have way to add remaining trade phases to new movement
636 637 638
      if not (link.getRelatedSimulationMovementValueList(explanation) and
              link.isCompleted(explanation)):
        remaining_trade_phase_list += link.getTradePhaseValueList()
639 640

      # collect to successor direction recursively
641
      state = link.getSuccessorValue()
642 643 644 645 646 647 648 649 650 651 652 653
      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

654
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradePhaseMovementList')
655 656
  def getTradePhaseMovementList(self, explanation, amount, trade_phase=None, delay_mode=None):
    """Returns a list of movement with appropriate arrow and dates,
657
    based on the Business Link definitions, provided 'amount' and optional
658 659 660
    trade phases. If no trade_phase is provided, the trade_phase defined
    on the Amount is used instead.
    
661 662
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680

    amount -- IAmount (quantity, resource) or IMovement

    trade_phase -- optional Trade Phase category

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
    """
    if trade_phase is None:
      trade_phase = amount.getTradePhase()

    if trade_phase is None: raise ValueError("A trade_phase must be defined on the Amount or provided to getTradePhaseMovementList")

    # Build a list of temp movements
    from Products.ERP5Type.Document import newTempMovement
    result = []
    id_index = 0
    base_id = amount.getId()
681
    for business_link in self.getTradeModelPathValueList(context=amount, trade_phase=trade_phase):
682
      id_index += 1
683 684
      movement = newTempMovement(business_link, '%s_%s' % (base_id, id_index))
      kw = self._getPropertyAndCategoryDict(explanation, amount, business_link, delay_mode=delay_mode)
685
      movement._edit(**kw)
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
      result.append(movement)

    # result can not be empty
    if not result: raise ValueError("A Business Process can not erase amounts")

    # Sort movement list and make sure the total is equal to total_quantity
    total_quantity = amount.getQuantity()
    current_quantity = 0
    result.sort(key=lambda x:x.getStartDate())
    stripped_result = []
    for movement in result:
      stripped_result.append(movement)
      quantity = movement.getQuantity()
      current_quantity += quantity
      if current_quantity > total_quantity: 
        # As soon as the current_quantity is greater than total_quantity
        # strip the result
        break

    # Make sure total_quantity is reached by changing last movement valye
    if current_quantity != total_quantity:
      movement._setQuantity(quantity + total_quantity - current_quantity)

    return stripped_result

711 712
  def _getPropertyAndCategoryDict(self, explanation, amount, business_link, delay_mode=None):
    """A private method to merge an amount and a business_link and return
713
    a dict of properties and categories which can be used to create a
714 715
    new movement.

716 717
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
718 719 720

    amount -- an IAmount instance or an IMovement instance
 
721
    business_link -- an IBusinessLink instance
722 723 724 725

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
    """
726 727 728 729
    if explanation.getPortalType() == "Applied Rule":
      rule = explanation.getSpecialiseValue()
    else:
      rule = None
730
    if rule is None:
731
      property_dict = _getPropertyAndCategoryList(amount)
732 733 734 735
    else:
      property_dict = {}
      for tester in rule._getUpdatingTesterList(exclude_quantity=False):
        property_dict.update(tester.getUpdatablePropertyDict(
736
          amount, None))
737 738 739

    # Arrow categories
    for base_category, category_url_list in \
740
            business_link.getArrowCategoryDict(context=amount).iteritems():
741 742 743 744
      property_dict[base_category] = category_url_list

    # Amount quantities - XXX-JPS maybe we should consider handling unit conversions here
    # and specifying units
745 746 747
    if business_link.getQuantity():
      property_dict['quantity'] = business_link.getQuantity()
    elif business_link.getEfficiency():
748
      property_dict['quantity'] = amount.getQuantity() *\
749
        business_link.getEfficiency()
750
    else:
751 752 753 754 755 756 757 758 759 760 761 762 763 764
      property_dict['quantity'] = amount.getQuantity()

    # Dates - the main concept of BPM is to search for reference dates
    # in parent simulation movements at expand time. This means that
    # a trade date which is based on a trade phase which is handled
    # by a child applied rule is not supported in ERP5 BPM.
    # In the same spirit, date calculation at expand time is local, not
    # global.
    if explanation.getPortalType() == 'Applied Rule':
      if explanation.getParentValue().getPortalType() != "Simulation Tool":
        # It only makes sens to search for start and start date for
        # applied rules which are not root applied rules. 
        # XXX-JPS could be extended with a rule property instead
        # of supports only in root applied rule case
765
        start_date, stop_date = self.getExpectedTradeModelPathStartAndStopDate(
766
                               explanation, business_link, delay_mode=delay_mode)
767 768 769 770
        property_dict['start_date'] = start_date
        property_dict['stop_date'] = stop_date
    else:
      raise TypeError("Explanation must be an Applied Rule in expand process") # Nothing to do
771

772
    # Set causality to business link
773
    property_dict['causality'] = business_link.getRelativeUrl() # XXX-JPS Will not work if we do not use real object
774 775 776

    return property_dict 

777
  # IBusinessProcess global API
778
  security.declareProtected(Permissions.AccessContentsInformation, 'isCompleted')
779
  def isCompleted(self, explanation):
780 781 782
    """Returns True is all applicable Trade States and Trade Phases
    are completed in the context of given explanation.

783 784
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
785
    """
786 787
    for state in self.getTradeStateList():
      if not state.isTradeStateCompleted(explanation):
788 789
        return False
    return True
790
  
791
  security.declareProtected(Permissions.AccessContentsInformation, 'isBuildable')
792
  def isBuildable(self, explanation):
793
    """Returns True is one Business Link of this Business Process
794
    is buildable in the context of given explanation.
795

796 797
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
798
    """
799
    return len(self.getBuildableBusinessLinkValueList(explanation)) != 0
800

801
  security.declareProtected(Permissions.AccessContentsInformation, 'isPartiallyBuildable')
802
  def isPartiallyBuildable(self, explanation):
803
    """Returns True is one Business Link of this Business Process
804 805
    is partially buildable in the context of given explanation.

806 807
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
808
    """
809
    return len(self.getPartiallyBuildableBusinessLinkValueList(explanation)) != 0
810

811
  security.declareProtected(Permissions.AccessContentsInformation, 'build')
812 813 814 815
  def build(self, explanation):
    """
      Build whatever is buildable
    """
816 817
    for business_link in self.getBuildableBusinessLinkValueList(explanation):
      business_link.build(explanation)