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

from AccessControl import ClassSecurityInfo

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

import zope.interface

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

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

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

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

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

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

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

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

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

  # Declarative interfaces
87
  zope.interface.implements(interfaces.ICategoryAccessProvider,
88 89 90 91 92
                            interfaces.IArrowBase,
                            interfaces.IBusinessPath,
                            interfaces.IBusinessBuildable,
                            interfaces.IBusinessCompletable
                            )
93

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

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

141
  # ICategoryAccessProvider overridden methods
142 143
  def _getCategoryMembershipList(self, category, **kw):
    """
144 145
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
146
    """
147
    context = kw.pop('context')
148
    result = Path._getCategoryMembershipList(self, category, **kw)
149 150
    if len(result) > 0:
      return result
151 152
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
153 154
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
155 156 157 158
    return result

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

172 173 174
  def _filterCategoryList(self, category_list, category, spec=(),
                          filter=None, portal_type=(), base=0,
                          keep_default=1, checked_permission=None):
175 176 177 178
    """
      XXX - implementation missing
      TBD - look at CategoryTool._buildFilter for inspiration
    """
179 180 181 182 183 184 185 186
    # basic filtering:
    #  * remove categories which base name is not category
    #  * respect base parameter
    prefix = category + '/'
    start_index = not base and len(prefix) or 0
    return [category[start_index:]
            for category in category_list
            if category.startswith(prefix)]
187 188 189

  # Dynamic context based categories
  def _getDynamicCategoryList(self, context):
190 191
    return self._getDynamicSourceCategoryList(context) \
         + self._getDynamicDestinationCategoryList(context)
192 193 194 195 196 197

  def _getDynamicSourceCategoryList(self, context):
    method_id = self.getSourceMethodId()
    if method_id:
      method = getattr(self, method_id)
      return method(context)
198
    return []
199 200 201 202 203 204

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

207
  # IBusinessBuildable implementation
208
  def isBuildable(self, explanation):
209 210
    """
    """
211 212 213 214 215
    # check if there is at least one simulation movement which is not
    # delivered
    result = False
    if self.isCompleted(explanation) or self.isFrozen(explanation):
      return False # No need to build what was already built or frozen
216
    for simulation_movement in self.getRelatedSimulationMovementValueList(
217 218 219
        explanation):
      if simulation_movement.getDeliveryValue() is None:
        result = True
220 221
    predecessor = self.getPredecessorValue()
    if predecessor is None:
222
      return result
223 224 225 226 227 228 229 230 231 232
    # XXX FIXME TODO
    # For now isPartiallyCompleted is used, as it was
    # assumed to not implement isPartiallyBuildable, so in reality
    # isBuildable is implemented like isPartiallyBuildable
    #
    # But in some cases it might be needed to implement
    # isPartiallyBuildable, than isCompleted have to be used here
    #
    # Such cases are Business Processes using sequence not related
    # to simulation tree with much of compensations
233
    if predecessor.isPartiallyCompleted(explanation):
234
      return result
235 236 237 238 239 240 241
    return False

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

242
  def _getExplanationUidList(self, explanation):
243
    """Helper method to fetch really explanation related movements"""
244
    explanation_uid_list = [explanation.getUid()]
245 246 247
    # XXX: getCausalityRelatedValueList is oversimplification, assumes
    #      that documents are sequenced like simulation movements, which
    #      is wrong
248
    for found_explanation in explanation.getCausalityRelatedValueList(
249 250
        portal_type=self.getPortalDeliveryTypeList()) + \
        explanation.getCausalityValueList():
251 252
      explanation_uid_list.extend(self._getExplanationUidList(
        found_explanation))
253 254
    return explanation_uid_list

255
  def build(self, explanation):
256 257 258
    """
      Build
    """
259
    builder_list = self.getDeliveryBuilderValueList() # Missing method
260
    for builder in builder_list:
261 262 263 264 265 266 267
      # chosen a way that builder is good enough to decide to select movements
      # which shall be really build (movement selection for build is builder
      # job, not business path job)
      builder.build(select_method_dict={
        'causality_uid': self.getUid(),
        'explanation_uid': self._getExplanationUidList(explanation)
      })
268 269

  # IBusinessCompletable implementation
270 271
  security.declareProtected(Permissions.AccessContentsInformation,
      'isCompleted')
272
  def isCompleted(self, explanation):
273 274 275 276
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
277
    acceptable_state_list = self.getCompletedStateList()
278
    for movement in self.getRelatedSimulationMovementValueList(explanation):
279 280 281 282
      if movement.getSimulationState() not in acceptable_state_list:
        return False
    return True

283 284
  security.declareProtected(Permissions.AccessContentsInformation,
      'isPartiallyCompleted')
285
  def isPartiallyCompleted(self, explanation):
286 287 288 289
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
290
    acceptable_state_list = self.getCompletedStateList()
291
    for movement in self.getRelatedSimulationMovementValueList(explanation):
292 293 294 295
      if movement.getSimulationState() in acceptable_state_list:
        return True
    return False

296 297
  security.declareProtected(Permissions.AccessContentsInformation,
      'isFrozen')
298
  def isFrozen(self, explanation):
299 300 301 302
    """
      Looks at all simulation related movements
      and checks if frozen
    """
303
    movement_list = self.getRelatedSimulationMovementValueList(explanation)
304 305 306 307 308 309 310
    if len(movement_list) == 0:
      return False # Nothing to be considered as Frozen
    for movement in movement_list:
      if not movement.isFrozen():
        return False
    return True

311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
  def _recurseGetValueList(self, document, portal_type):
    """Helper method to recurse documents as deep as possible and returns
       list of document values matching portal_type"""
    return_list = []
    for subdocument in document.contentValues():
      if subdocument.getPortalType() == portal_type:
        return_list.append(subdocument)
      return_list.extend(self._recurseGetValueList(subdocument, portal_type))
    return return_list

  def isMovementRelatedWithMovement(self, movement_value_a, movement_value_b):
    """Documentation in IBusinessPath"""
    movement_a_path_list = movement_value_a.getRelativeUrl().split('/')
    movement_b_path_list = movement_value_b.getRelativeUrl().split('/')

    if len(movement_a_path_list) == len(movement_b_path_list):
      if movement_value_a == movement_value_b:
        # same is related
        return True
      # same level, cannot be related
      return False

    index = 0
    for movement_a_part in movement_a_path_list:
      try:
        movement_b_part = movement_b_path_list[index]
      except IndexError:
        # so far was good, they are related
        return True
      if movement_a_part != movement_b_part:
        return False
      index += 1
    # movement_a_path_list was shorter than movement_b_path_list and matched
    # so they are related
    return True

  def _isDeliverySimulationMovementRelated(self, delivery, simulation_movement):
    """Helper method, which checks if simulation_movement is BPM like related
       with delivery"""
    for delivery_simulation_movement in self \
        ._getDeliverySimulationMovementList(delivery):
      if self.isMovementRelatedWithMovement(delivery_simulation_movement,
          simulation_movement):
        return True
    return False

  def _getDeliverySimulationMovementList(self, delivery):
    """Returns list of simulation movements related to delivery by applied rule
       or delivery's movements"""
    movement_list = []
    for applied_rule in delivery.getCausalityRelatedValueList(
        portal_type='Applied Rule'):
      movement_list.extend(applied_rule.contentValues(
        portal_type='Simulation Movement'))
    for movement in delivery.getMovementList():
      movement_list.extend(movement.getDeliveryRelatedValueList(
        portal_type='Simulation Movement'))
    return movement_list

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

      As business sequence is not related to simulation tree need to built
      full simulation trees per applied rule
379
    """
380
    # FIXME: Needed better implementation, maybe use catalog?
381
    simulation_movement_value_list = []
382
    # first tree from root Applied Rules related to delivery itself
383 384
    for applied_rule in explanation.getCausalityRelatedValueList(
        portal_type='Applied Rule'):
385 386 387 388
      simulation_movement_value_list.extend(self._recurseGetValueList(
        applied_rule, 'Simulation Movement'))
    # now tree from root Applied Rules related to movements used to build delivery
    root_applied_rule_list = []
389
    for movement in explanation.getMovementList():
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
      for simulation_movement in movement.getDeliveryRelatedValueList(
          portal_type='Simulation Movement'):
        applied_rule = simulation_movement.getRootAppliedRule()
        if applied_rule not in root_applied_rule_list:
          root_applied_rule_list.append(
              simulation_movement.getRootAppliedRule())

    for applied_rule in root_applied_rule_list:
      simulation_movement_value_list.extend(self._recurseGetValueList(
        applied_rule, 'Simulation Movement'))

    return [simulation_movement.getObject() for simulation_movement
          in simulation_movement_value_list
          # this business path
          if simulation_movement.getCausalityValue() == self
          # related with explanation
          and self._isDeliverySimulationMovementRelated(
            explanation, simulation_movement)]
408

409
  def getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
410 411 412 413 414 415 416
    """
      Returns the expected start date for this
      path based on the explanation.

      predecessor_date -- if provided, computes the date base on the
                          date value provided
    """
417 418 419 420 421 422 423 424 425 426 427
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStartDate,
                                 self._getPredecessorExpectedStartDate,
                                 self._getSuccessorExpectedStartDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

  def _getRootExplanationExpectedStartDate(self, explanation, *args, **kwargs):
    if self.getParentValue().isStartDateReferential():
      return explanation.getStartDate()
    else:
428 429 430
      expected_date = self.getExpectedStopDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
431 432

  def _getPredecessorExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
433
    if predecessor_date is None:
434 435 436 437 438 439 440 441 442
      node = self.getPredecessorValue()
      if node is not None:
        predecessor_date = node.getExpectedCompletionDate(explanation, *args, **kwargs)
    if predecessor_date is not None:
      return predecessor_date + self.getWaitTime()

  def _getSuccessorExpectedStartDate(self, explanation, *args, **kwargs):
    node = self.getSuccessorValue()
    if node is not None:
443 444 445
      expected_date =  node.getExpectedBeginningDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
446 447

  def getExpectedStopDate(self, explanation, predecessor_date=None, *args, **kwargs):
448 449 450 451 452 453 454
    """
      Returns the expected stop date for this
      path based on the explanation.

      predecessor_date -- if provided, computes the date base on the
                          date value provided
    """
455 456 457 458 459 460 461 462 463 464 465
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStopDate,
                                 self._getPredecessorExpectedStopDate,
                                 self._getSuccessorExpectedStopDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

  def _getRootExplanationExpectedStopDate(self, explanation, *args, **kwargs):
    if self.getParentValue().isStopDateReferential():
      return explanation.getStopDate()
    else:
466 467 468
      expected_date = self.getExpectedStartDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getLeadTime()
469 470 471 472

  def _getPredecessorExpectedStopDate(self, explanation, *args, **kwargs):
    node = self.getPredecessorValue()
    if node is not None:
473 474 475
      expected_date = node.getExpectedCompletionDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getWaitTime() + self.getLeadTime()
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504

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

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

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

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

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

505 506
    predecessor_expected_date = predecessor_method(
      explanation, visited=visited, *args, **kwargs)
507

508 509
    successor_expected_date = successor_method(
      explanation, visited=visited, *args, **kwargs)
510 511 512 513 514 515 516 517 518 519 520 521 522

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