BusinessProcess.py 38.5 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
from zLOG import LOG
41

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

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
  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, 
66
  it is necessary to ignore certain business link to evaluate
67
  completion or completion dates. This is very true for Union 
68 69
  Business Processes. This is the concept of Business Link closure,
  ie. filtering out all Business Link which are not used in an explanation.
70

71 72 73 74 75 76 77 78 79 80
  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)
81 82 83 84 85
  - 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
86 87
  - why are we using objectValues in some places ?
  - add a property to rules in order to determine whether dates
88
    are provided by the rule or by business link / trade model path. This is an extension
89 90 91 92
    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
93
  - support conversions in trade model path
94 95

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

116 117 118 119
  # Declarative interfaces
  zope.interface.implements(interfaces.IBusinessProcess,
                            interfaces.IArrowBase)

120 121 122 123 124 125 126 127 128 129 130 131 132
  # 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
    """
133
    if trade_phase is not None:
134
      if isinstance(trade_phase, basestring):
135
        trade_phase = (trade_phase,)
Julien Muchembled's avatar
Julien Muchembled committed
136 137
      trade_phase = set(x.split('trade_phase/', 1)[-1]
                        for x in trade_phase)
138 139 140 141 142
    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
143 144 145 146 147 148
    LOG('self', 0, repr(self))
    LOG('objectValues', 0, repr(self.objectValues()))
    LOG('portal_type', 0, repr(kw['portal_type']))
    LOG('objectValues kw', 0, repr(self.objectValues(**kw)))
    LOG('trade_phase', 0, trade_phase)
    LOG('original_path_list', 0, original_path_list)
149 150 151 152 153
    # 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:
Julien Muchembled's avatar
Julien Muchembled committed
154 155
      # Filter our business path which trade phase does not match
      if trade_phase is None or trade_phase.intersection(path.getTradePhaseList()):
156
        path_list.append(path)
157
    LOG('path_list', 0, path_list)
158 159 160 161
    # 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?
Julien Muchembled's avatar
Julien Muchembled committed
162
    result = []
163 164 165
    for path in path_list:
      if path.test(context):
        result.append(path)
166
    LOG('result', 0, result)
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
    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')

197
    explanation_cache = _getExplanationCache(explanation)
198
    LOG('calling explanation_cache.getReferenceDate', 0, '%s %s %s %s' % (explanation, self, trade_date, reference_date_method_id))
199 200
    reference_date = explanation_cache.getReferenceDate(self, trade_date, reference_date_method_id)

201 202
    # Computer start_date and stop_date (XXX-JPS this could be cached and accelerated)    
    start_date = reference_date + trade_model_path.getPaymentTerm(0.0) # XXX-JPS Until better naming
203
    if delay_mode == 'min':
204
      delay = trade_model_path.getMinDelay(0.0)
205
    elif delay_mode == 'max':
206
      delay = trade_model_path.getMaxDelay(0.0)
207
    else:
208
      delay = (trade_model_path.getMaxDelay(0.0) + trade_model_path.getMinDelay(0.0)) / 2.0
209 210 211 212
    stop_date = start_date + delay
        
    return start_date, stop_date

213 214 215
  # IBusinessLinkProcess implementation
  security.declareProtected(Permissions.AccessContentsInformation, 'getBusinessLinkValueList')
  def getBusinessLinkValueList(self, trade_phase=None, context=None,
216
                               predecessor=None, successor=None, **kw):
217
    """Returns all Business Links of the current BusinessProcess which
218 219 220 221
    are matching the given trade_phase and the optional context.

    trade_phase -- filter by trade phase

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

225
    predecessor -- filter by trade state predecessor
226

227
    successor -- filter by trade state successor
228

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

267
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkCompleted')
268 269
  def isBusinessLinkCompleted(self, explanation, business_link):
    """Returns True if given Business Link document
270 271
    is completed in the context of provided explanation.

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

275
    business_link -- a Business Link document
276
    """
277
    LOG('In isBusinessLinkCompleted', 0, repr(business_link))
278 279
    # Return False if Business Link is not completed
    if not business_link.isCompleted(explanation):
280
      return False
281
    predecessor_state = business_link.getPredecessor()
282
    if not predecessor_state:
283
      # This is a root business links, no predecessor
284 285 286 287 288 289 290
      # 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
291
      # a business link may be completed even if its predecessor state
292 293 294
      # is not
      return True
    # Build the closure business process which only includes those business 
295
    # links wich are directly related to the current business link but DO NOT 
296 297
    # narrow down the explanation else we might narrow down so much that
    # it becomes an empty set
298
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
299 300
    return closure_process.isTradeStateCompleted(explanation, predecessor_state)

301
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkPartiallyCompleted')
302 303
  def isBusinessLinkPartiallyCompleted(self, explanation, business_link):
    """Returns True if given Business Link document
304 305
    is partially completed in the context of provided explanation.

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

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

335
  # IBuildableBusinessLinkProcess implementation
336
  security.declareProtected(Permissions.AccessContentsInformation, 'getBuildableBusinessLinkValueList')
337 338
  def getBuildableBusinessLinkValueList(self, explanation):
    """Returns the list of Business Link which are buildable
339
    by taking into account trade state dependencies between
340
    Business Link.
341

342 343
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
344
    """
345
    result = []
346 347 348
    for business_link in self.getBusinessLinkValueList():
      if self.isBusinessLinkBuildable(explanation, business_link):
        result.append(business_link)
349 350
    return result

351
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyBuildableBusinessLinkValueList')
352 353
  def getPartiallyBuildableBusinessLinkValueList(self, explanation):
    """Returns the list of Business Link which are partially buildable
354
    by taking into account trade state dependencies between
355
    Business Link.
356

357 358
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
359
    """
360
    result = []
361 362 363
    for business_link in self.getBusinessLinkValueList():
      if self.isBusinessLinkPartiallyBuildable(explanation, business_link):
        result.append(business_link)
364 365
    return result

366
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkBuildable')
367
  def isBusinessLinkBuildable(self, explanation, business_link):
368 369 370
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is completed.

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

374
    business_link -- a Business Link document
375 376
    """
    # If everything is delivered, no need to build
377
    LOG('In isBusinessLinkBuildable', 0, repr(business_link))
378
    if business_link.isDelivered(explanation):
379
      LOG('In isBusinessLinkBuildable', 0, 'business link is delivered and thus False')
380 381
      return False
    # We must take the closure cause only way to combine business process
382 383
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
    predecessor = business_link.getPredecessor()
384
    return closure_process.isTradeStateCompleted(explanation, predecessor)
385

386
  security.declareProtected(Permissions.AccessContentsInformation, 'isBusinessLinkPartiallyBuildable')
387
  def isBusinessLinkPartiallyBuildable(self, explanation, business_link):
388 389 390
    """Returns True if any of the related Simulation Movement
    is buildable and if the predecessor trade state is partially completed.

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

394
    business_link -- a Business Link document
395 396
    """
    # If everything is delivered, no need to build
397
    if business_link.isDelivered(explanation):
398 399
      return False
    # We must take the closure cause only way to combine business process
400 401
    closure_process = _getBusinessLinkClosure(self, explanation, business_link)
    predecessor = business_link.getPredecessor()
402 403 404
    return closure_process.isTradeStatePartiallyCompleted(predecessor)

  # ITradeStateProcess implementation
405
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradeStateList')
406
  def getTradeStateList(self):
407 408
    """Returns list of all trade_state of this Business Process
    by looking at successor and predecessor values of contained
409
    Business Link.
410

411 412
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
413 414
    """
    result = set()
415 416 417
    for business_link in self.getBusinessLinkValueList():
      result.add(business_link.getPredecessor())
      result.add(business_link.getSuccessor())
418 419
    return result

420
  security.declareProtected(Permissions.AccessContentsInformation, 'isInitialTradeState')
421 422
  def isInitialTradeState(self, trade_state):
    """Returns True if given 'trade_state' has no successor related
423
    Business Link.
424 425 426

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

429
  security.declareProtected(Permissions.AccessContentsInformation, 'isFinalTradeState')
430 431
  def isFinalTradeState(self, trade_state):
    """Returns True if given 'trade_state' has no predecessor related
432
    Business Link.
433 434 435

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

438
  security.declareProtected(Permissions.AccessContentsInformation, 'getSuccessorTradeStateList')
439
  def getSuccessorTradeStateList(self, explanation, trade_state):
440 441
    """Returns the list of successor states in the 
    context of given explanation. This list is built by looking
442
    at all successor of business link involved in given explanation
443 444
    and which predecessor is the given trade_phase.

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

    trade_state -- a Trade State category
449
    """
450
    result = set()
451 452 453
    for business_link in self.getBusinessLinkValueList():
      if business_link.getPredecessor() == trade_state:
        result.add(business_link.getSuccessor())
454
    return result
455

456
  security.declareProtected(Permissions.AccessContentsInformation, 'getPredecessorTradeStateList')
457
  def getPredecessorTradeStateList(self, explanation, trade_state):
458 459
    """Returns the list of predecessor states in the 
    context of given explanation. This list is built by looking
460
    at all predecessor of business link involved in given explanation
461 462
    and which sucessor is the given trade_phase.

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

    trade_state -- a Trade State category
467
    """
468
    result = set()
469 470 471
    for business_link in self.getBusinessLinkValueList():
      if business_link.getSuccessor() == trade_state:
        result.add(business_link.getPredecessor())
472 473
    return result

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

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

484
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyCompletedTradeStateList')
485
  def getPartiallyCompletedTradeStateList(self, explanation):
486 487
    """Returns the list of Trade States which are partially 
    completed in the context of given explanation.
488

489 490
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
491
    """
492 493
    return filter(lambda x:self.isTradeStatePartiallyCompleted(explanation, x), self.getTradeStateList())

494
  security.declareProtected(Permissions.AccessContentsInformation, 'getLatestCompletedTradeStateList')
495
  def getLatestCompletedTradeStateList(self, explanation):
496 497 498 499
    """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.

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

510
  security.declareProtected(Permissions.AccessContentsInformation, 'getLatestPartiallyCompletedTradeStateList')
511
  def getLatestPartiallyCompletedTradeStateList(self, explanation):
512 513 514 515
    """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.

516 517
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
518
    """
519 520
    result = set()
    for state in self.getCompletedTradeStateValue(explanation):
521 522
      for business_link in state.getPredecessorRelatedValueList():
        if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
523
          result.add(state)
524 525
    return result

526
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradeStateCompleted')
527
  def isTradeStateCompleted(self, explanation, trade_state):
528 529 530 531
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is completed
    in the context of given explanation.

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

    trade_state -- a Trade State category
536
    """
537
    LOG('In isTradeStateCompleted', 0, repr(trade_state))
538
    for business_link in self.getBusinessLinkValueList(successor=trade_state):
539
      if not self.isBusinessLinkCompleted(explanation, business_link):
540
        LOG('A business link is not completed', 0, repr(business_link))
541 542
        return False
    return True      
543

544
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradeStatePartiallyCompleted')
545
  def isTradeStatePartiallyCompleted(self, explanation, trade_state):
546 547 548
    """Returns True if all predecessor trade states are
    completed and if no successor trade state is partially completed
    in the context of given explanation.
549

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

553
    trade_state -- a Trade State category
554
    """
555 556
    for business_link in self.getBusinessLinkValueList(successor=trade_state):
      if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
557 558 559 560
        return False
    return True      

  # ITradePhaseProcess implementation
561
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradePhaseList')
562
  def getTradePhaseList(self):
563
    """Returns list of all trade_phase of this Business Process
564
    by looking at trade_phase values of contained Business Link.
565
    """
566
    result = set()
567 568
    for business_link in self.getBusinessLinkValueList():
      result = result.union(business_link.getTradePhaseList())
569 570
    return result

571
  security.declareProtected(Permissions.AccessContentsInformation, 'getCompletedTradePhaseList')
572
  def getCompletedTradePhaseList(self, explanation):
573 574 575
    """Returns the list of Trade Phases which are completed
    in the context of given explanation.

576 577
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
578
    """
579
    return filter(lambda x:self.isTradePhaseCompleted(explanation, x), self.getTradePhaseList())
580
    
581
  security.declareProtected(Permissions.AccessContentsInformation, 'getPartiallyCompletedTradePhaseList')
582
  def getPartiallyCompletedTradePhaseList(self, explanation):
583 584
    """Returns the list of Trade Phases which are partially completed
    in the context of given explanation. 
585

586 587
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
588
    """
589
    return filter(lambda x:self.isTradePhasePartiallyCompleted(explanation, x), self.getTradePhaseList())
590

591
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradePhaseCompleted')
592
  def isTradePhaseCompleted(self, explanation, trade_phase):
593
    """Returns True all business link with given trade_phase
594 595
    applicable to given explanation are completed.

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

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

606
  security.declareProtected(Permissions.AccessContentsInformation, 'isTradePhasePartiallyCompleted')
607
  def isTradePhasePartiallyCompleted(self, explanation, trade_phase):
608
    """Returns True at least one business link with given trade_phase
609 610
    applicable to given explanation is partially completed
    or completed.
611

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

615 616
    trade_phase -- a Trade Phase category
    """
617 618
    for business_link in self.getBusinessLinkValueList(trade_phase=trade_phase):
      if not self.isBusinessLinkPartiallyCompleted(explanation, business_link):
619 620
        return False
    return True
621

622
  security.declareProtected(Permissions.AccessContentsInformation, 'getRemainingTradePhaseList')
623
  def getRemainingTradePhaseList(self, explanation, business_link, trade_phase_list=None):
624 625
    """Returns the list of remaining trade phases which to be achieved
    as part of a business process. This list is calculated by analysing 
626 627
    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
628 629 630
    method is useful mostly for production and MRP to manage a distributed
    supply and production chain.

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

634
    business_link -- a Business Link document
635 636

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

639 640
    NOTE: this code has not been reviewed and needs review

641 642
    NOTE: explanation is not involved here because we consider here that
    self is the result of asUnionBusinessProcess and thus only contains
643
    applicable Business Link to a given simulation subtree. Since the list
644 645 646
    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.
647 648
    """
    remaining_trade_phase_list = []
649
    trade_state = business_link.getSuccessor()
650
    for link in [x for x in self.objectValues(portal_type="Business Link") \
651
        if x.getPredecessor() == trade_state]:
652
      # XXX When no simulations related to link, what should link.isCompleted return?
653
      #     if True we don't have way to add remaining trade phases to new movement
654 655 656
      if not (link.getRelatedSimulationMovementValueList(explanation) and
              link.isCompleted(explanation)):
        remaining_trade_phase_list += link.getTradePhaseValueList()
657 658

      # collect to successor direction recursively
659
      state = link.getSuccessorValue()
660 661 662 663 664 665 666 667 668 669 670 671
      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

672
  security.declareProtected(Permissions.AccessContentsInformation, 'getTradePhaseMovementList')
673 674
  def getTradePhaseMovementList(self, explanation, amount, trade_phase=None, delay_mode=None,
                                      update_property_dict=None):
675
    """Returns a list of movement with appropriate arrow and dates,
676
    based on the Business Link definitions, provided 'amount' and optional
677 678 679
    trade phases. If no trade_phase is provided, the trade_phase defined
    on the Amount is used instead.
    
680 681
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
682 683 684 685 686 687 688

    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
689 690
                  
    update_property_method -- 
691
    """
692
    if not trade_phase:
693
      trade_phase = amount.getTradePhase()
694 695
      if not trade_phase:
        raise ValueError("A trade_phase must be defined on the Amount or provided to getTradePhaseMovementList")
696 697 698 699 700 701

    # Build a list of temp movements
    from Products.ERP5Type.Document import newTempMovement
    result = []
    id_index = 0
    base_id = amount.getId()
702
    if update_property_dict is None: update_property_dict = {}
703
    for trade_model_path in self.getTradeModelPathValueList(context=amount, trade_phase=trade_phase):
704
      id_index += 1
705 706
      movement = newTempMovement(trade_model_path, '%s_%s' % (base_id, id_index))
      kw = self._getPropertyAndCategoryDict(explanation, amount, trade_model_path, delay_mode=delay_mode)
707
      kw.update(update_property_dict)
708
      movement._edit(**kw)
709 710 711
      business_link = self.getBusinessLinkValueList(trade_phase=trade_phase, context=movement)
      business_link = map(lambda x: x.getRelativeUrl(), business_link)
      movement._setCausalityList(business_link + movement.getCausalityList())
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
      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

737
  def _getPropertyAndCategoryDict(self, explanation, amount, trade_model_path, delay_mode=None):
738
    """A private method to merge an amount and a business_link and return
739
    a dict of properties and categories which can be used to create a
740 741
    new movement.

742 743
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
744 745 746

    amount -- an IAmount instance or an IMovement instance
 
747
    trade_model_path -- an ITradeModelPath instance
748 749 750 751

    delay_mode -- optional value to specify calculation mode ('min', 'max')
                  if no value specified use average delay
    """
752 753 754 755
    if explanation.getPortalType() == "Applied Rule":
      rule = explanation.getSpecialiseValue()
    else:
      rule = None
756
    if rule is None:
757
      property_dict = _getPropertyAndCategoryList(amount)
758 759 760 761
    else:
      property_dict = {}
      for tester in rule._getUpdatingTesterList(exclude_quantity=False):
        property_dict.update(tester.getUpdatablePropertyDict(
762
          amount, None))
763 764 765

    # Arrow categories
    for base_category, category_url_list in \
766
            trade_model_path.getArrowCategoryDict(context=amount).iteritems():
767 768 769 770
      property_dict[base_category] = category_url_list

    # Amount quantities - XXX-JPS maybe we should consider handling unit conversions here
    # and specifying units
771 772
    if trade_model_path.getQuantity():
      property_dict['quantity'] = trade_model_path.getQuantity()
773
    elif trade_model_path.getEfficiency():
774
      property_dict['quantity'] = amount.getQuantity() *\
775
        trade_model_path.getEfficiency()
776
    else:
777 778 779 780 781 782 783 784 785 786 787 788 789 790
      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
791
        start_date, stop_date = self.getExpectedTradeModelPathStartAndStopDate(
792
                               explanation, trade_model_path, delay_mode=delay_mode)
793 794 795 796
        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
797

798 799
    # Set causality to trade model path
    property_dict['causality'] = trade_model_path.getRelativeUrl() # XXX-JPS Will not work if we do not use real object
800
    #(ie. if we use kind of 'temp')
801

802 803 804
    # Set trade_phase to the trade phase of trade_model_path
    property_dict['trade_phase'] = trade_model_path.getTradePhase()
    
805 806
    return property_dict 

807
  # IBusinessProcess global API
808
  security.declareProtected(Permissions.AccessContentsInformation, 'isCompleted')
809
  def isCompleted(self, explanation):
810 811 812
    """Returns True is all applicable Trade States and Trade Phases
    are completed in the context of given explanation.

813 814
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
815
    """
816 817
    for state in self.getTradeStateList():
      if not state.isTradeStateCompleted(explanation):
818 819
        return False
    return True
820
  
821
  security.declareProtected(Permissions.AccessContentsInformation, 'isBuildable')
822
  def isBuildable(self, explanation):
823
    """Returns True is one Business Link of this Business Process
824
    is buildable in the context of given explanation.
825

826 827
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
828
    """
829
    return len(self.getBuildableBusinessLinkValueList(explanation)) != 0
830

831
  security.declareProtected(Permissions.AccessContentsInformation, 'isPartiallyBuildable')
832
  def isPartiallyBuildable(self, explanation):
833
    """Returns True is one Business Link of this Business Process
834 835
    is partially buildable in the context of given explanation.

836 837
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
838
    """
839
    return len(self.getPartiallyBuildableBusinessLinkValueList(explanation)) != 0
840

841
  security.declareProtected(Permissions.AccessContentsInformation, 'build')
842 843 844 845
  def build(self, explanation):
    """
      Build whatever is buildable
    """
846
    LOG('In business process build', 0, repr(explanation))
847
    for business_link in self.getBuildableBusinessLinkValueList(explanation):
848
      business_link.build(explanation=explanation)