BusinessProcess.py 38.4 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)
Jérome Perrin's avatar
Jérome Perrin committed
138 139
    kw.setdefault('portal_type', self.getPortalTradeModelPathTypeList())
    kw.setdefault('sort_on', 'int_index')
140
    original_path_list = self.objectValues(**kw) # Why Object Values ??? XXX-JPS
141 142 143 144 145 146
    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)
147 148 149 150 151
    # 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
152 153
      # Filter our business path which trade phase does not match
      if trade_phase is None or trade_phase.intersection(path.getTradePhaseList()):
154
        path_list.append(path)
155
    LOG('path_list', 0, path_list)
156 157 158 159
    # 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?
Jérome Perrin's avatar
Jérome Perrin committed
160
    return [path for path in path_list if path.test(context)]
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182

  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()
Julien Muchembled's avatar
Julien Muchembled committed
183
    assert trade_date, 'a trade_date must be defined on the Trade Model Path'
184 185 186 187 188

    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')

189
    explanation_cache = _getExplanationCache(explanation)
190
    LOG('calling explanation_cache.getReferenceDate', 0, '%s %s %s %s' % (explanation, self, trade_date, reference_date_method_id))
191 192
    reference_date = explanation_cache.getReferenceDate(self, trade_date, reference_date_method_id)

193 194
    # 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
195
    if delay_mode == 'min':
196
      delay = trade_model_path.getMinDelay(0.0)
197
    elif delay_mode == 'max':
198
      delay = trade_model_path.getMaxDelay(0.0)
199
    else:
200
      delay = (trade_model_path.getMaxDelay(0.0) + trade_model_path.getMinDelay(0.0)) / 2.0
201 202 203 204
    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 not None:
Julien Muchembled's avatar
Julien Muchembled committed
224
      if isinstance(trade_phase, basestring):
225 226 227
        trade_phase = set((trade_phase,))
      else:
        trade_phase = set(trade_phase)
Jérome Perrin's avatar
Jérome Perrin committed
228 229
    kw.setdefault('portal_type', self.getPortalBusinessLinkTypeList())
    kw.setdefault('sort_on', 'int_index')
230
    original_business_link_list = self.objectValues(**kw) # Why Object Values ??? XXX-JPS
231
    # Separate the selection of business links into two steps
232
    # for easier debugging.
233
    # First, collect business links which can be applicable to a given context.
234 235 236
    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
237
        continue # Filter our business link which predecessor does not match
238
      if successor is not None and business_link.getSuccessor() != successor:
Julien Muchembled's avatar
Julien Muchembled committed
239
        continue # Filter our business link which successor does not match
Jérome Perrin's avatar
Jérome Perrin committed
240 241
      if trade_phase is not None and not trade_phase.intersection(
                                   business_link.getTradePhaseList()):
Julien Muchembled's avatar
Julien Muchembled committed
242 243
        continue # Filter our business link which trade phase does not match
      business_link_list.append(business_link)
244 245
    # Then, filter business links by Predicate API.
    # FIXME: Ideally, we should use the Domain Tool to search business links,
246 247
    # and avoid using the low level Predicate API. But the Domain Tool does
    # support the condition above without scripting?
248 249 250 251
    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
Jérome Perrin's avatar
Jérome Perrin committed
252 253
    return [business_link for business_link in business_link_list
                if business_link.test(context)]
254

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

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

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

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

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

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

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

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

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

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

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

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

362
    business_link -- a Business Link document
363 364
    """
    # If everything is delivered, no need to build
365
    LOG('In isBusinessLinkBuildable', 0, repr(business_link))
366
    if business_link.isDelivered(explanation):
367
      LOG('In isBusinessLinkBuildable', 0, 'business link is delivered and thus False')
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
    return closure_process.isTradeStateCompleted(explanation, predecessor)
373

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
    LOG('In isTradeStateCompleted', 0, repr(trade_state))
526
    for business_link in self.getBusinessLinkValueList(successor=trade_state):
527
      if not self.isBusinessLinkCompleted(explanation, business_link):
528
        LOG('A business link is not completed', 0, repr(business_link))
529 530
        return False
    return True      
531

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

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

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

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

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

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

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

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

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

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

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

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

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

610
  security.declareProtected(Permissions.AccessContentsInformation, 'getRemainingTradePhaseList')
611
  def getRemainingTradePhaseList(self, explanation, business_link, trade_phase_list=None):
612 613
    """Returns the list of remaining trade phases which to be achieved
    as part of a business process. This list is calculated by analysing 
614 615
    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
616 617 618
    method is useful mostly for production and MRP to manage a distributed
    supply and production chain.

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

622
    business_link -- a Business Link document
623 624

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

627 628
    NOTE: this code has not been reviewed and needs review

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

      # collect to successor direction recursively
647
      state = link.getSuccessorValue()
648 649 650 651 652 653 654 655 656 657 658 659
      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

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

    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
677 678
                  
    update_property_method -- 
679
    """
680
    if not trade_phase:
681
      trade_phase = amount.getTradePhase()
682
      if not trade_phase:
Romain Courteaud's avatar
Romain Courteaud committed
683 684 685
        raise ValueError("%s: a trade_phase must be defined on the " \
                         "Amount or provided to getTradePhaseMovementList" %
                          amount.getRelativeUrl())
686 687 688 689 690 691

    # Build a list of temp movements
    from Products.ERP5Type.Document import newTempMovement
    result = []
    id_index = 0
    base_id = amount.getId()
692
    if update_property_dict is None: update_property_dict = {}
693
    for trade_model_path in self.getTradeModelPathValueList(context=amount, trade_phase=trade_phase):
694
      id_index += 1
695 696
      movement = newTempMovement(trade_model_path, '%s_%s' % (base_id, id_index))
      kw = self._getPropertyAndCategoryDict(explanation, amount, trade_model_path, delay_mode=delay_mode)
697 698 699 700 701
      try:
        kw['trade_phase'], = \
          set(trade_phase).intersection(trade_model_path.getTradePhaseList())
      except ValueError:
        pass
702
      kw.update(update_property_dict)
703
      movement._edit(**kw)
704 705
      business_link = self.getBusinessLinkValueList(trade_phase=trade_phase,
                                                    context=movement)
706 707 708
      movement._setCausalityList([trade_model_path.getRelativeUrl()]
        + [x.getRelativeUrl() for x in business_link]
        + movement.getCausalityList())
709 710
      result.append(movement)

711 712 713
    if not explanation.getSpecialiseValue().getSameTotalQuantity():
      return result

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

718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
    # 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

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

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

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

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

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

    # Amount quantities - XXX-JPS maybe we should consider handling unit conversions here
    # and specifying units
772 773
    if trade_model_path.getQuantity():
      property_dict['quantity'] = trade_model_path.getQuantity()
774
    elif trade_model_path.getEfficiency():
775
      property_dict['quantity'] = amount.getQuantity() *\
776
        trade_model_path.getEfficiency()
777
    else:
778 779 780 781 782 783 784 785 786 787
      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":
Julien Muchembled's avatar
Julien Muchembled committed
788 789 790 791 792 793 794 795 796
        # It only makes sense to search for start and stop dates for
        # applied rules which are not root applied rules.
        # Date calculation by Business Process can be also disabled by
        # leaving 'trade_phase' unset (XXX: a separate boolean property,
        # on the TMP or the rule, may be better).
        if trade_model_path.getTradeDate():
          property_dict['start_date'], property_dict['stop_date'] = \
            self.getExpectedTradeModelPathStartAndStopDate(
              explanation, trade_model_path, delay_mode=delay_mode)
797 798
    else:
      raise TypeError("Explanation must be an Applied Rule in expand process") # Nothing to do
799
    return property_dict
800

801
  # IBusinessProcess global API
802
  security.declareProtected(Permissions.AccessContentsInformation, 'isCompleted')
803
  def isCompleted(self, explanation):
804 805 806
    """Returns True is all applicable Trade States and Trade Phases
    are completed in the context of given explanation.

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

820 821
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
822
    """
823
    return len(self.getBuildableBusinessLinkValueList(explanation)) != 0
824

825
  security.declareProtected(Permissions.AccessContentsInformation, 'isPartiallyBuildable')
826
  def isPartiallyBuildable(self, explanation):
827
    """Returns True is one Business Link of this Business Process
828 829
    is partially buildable in the context of given explanation.

830 831
    explanation -- an Order, Order Line, Delivery or Delivery Line or
                   Applied Rule which implicitely defines a simulation subtree
832
    """
833
    return len(self.getPartiallyBuildableBusinessLinkValueList(explanation)) != 0
834

835
  security.declareProtected(Permissions.AccessContentsInformation, 'build')
836 837 838 839
  def build(self, explanation):
    """
      Build whatever is buildable
    """
840
    LOG('In business process build', 0, repr(explanation))
841
    for business_link in self.getBuildableBusinessLinkValueList(explanation):
842
      business_link.build(explanation=explanation)