BusinessPath.py 23.9 KB
Newer Older
1
# -*- coding: shift_jis -*-
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
from Products.ERP5.Document.Path import Path
36
from Products.ERP5.Document.Predicate import Predicate
37
from Products.ERP5.ExplanationCache import _getExplanationCache
38 39 40

import zope.interface

41 42
from zLOG import LOG

43
class BusinessPath(Path, Predicate):
44
  """
45
    The BusinessPath class embeds all information related to
46
    lead times and parties involved at a given phase of a business
47 48
    process.

49
    BusinessPath are also used as helper to build deliveries from
50 51
    buildable movements.

52 53
    The idea is to invoke isBuildable() on the collected simulation
    movements (which are orphan) during build "after select" process
54

55
    Here is the typical code of an alarm in charge of the building process::
56

57 58 59
      builder = portal_deliveries.a_delivery_builder
      for business_path in builder.getDeliveryBuilderRelatedValueList():
        builder.build(causality_uid=business_path.getUid(),) # Select movements
60

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

66
    TODO:
Łukasz Nowak's avatar
Łukasz Nowak committed
67 68
      - merge build implementation from erp5_bpm business template to ERP5
        product code with backward compatibility
69 70 71 72 73 74 75 76 77 78 79 80 81 82
  """
  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
83
                    , PropertySheet.Reference
84 85
                    , PropertySheet.Comment
                    , PropertySheet.Arrow
86
                    , PropertySheet.Amount
87
                    , PropertySheet.Chain # XXX-JPS Why N
88
                    , PropertySheet.SortIndex
89
                    , PropertySheet.BusinessPath
90
                    , PropertySheet.FlowCapacity
91
                    , PropertySheet.Reference
92
                    , PropertySheet.PaymentCondition # XXX-JPS must be renames some day
93 94 95
                    )

  # Declarative interfaces
96
  zope.interface.implements(interfaces.ICategoryAccessProvider,
97 98
                            interfaces.IArrowBase,
                            interfaces.IBusinessPath,
99
                            interfaces.IPredicate,
100
                            )
101

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

126 127 128
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getDestinationArrowBaseCategoryList')
  def getDestinationArrowBaseCategoryList(self):
129 130 131 132
    """
      Returns all categories which are used to define the destination
      of this Arrow
    """
133
    # Naive implementation - we must use category groups instead - XXX-JPS review this later
134 135 136
    return ('destination',
            'destination_account',
            'destination_administration',
137 138 139
            #'destination_advice',
            #'destination_carrier',
            #'destination_decision',
140 141 142
            'destination_function',
            'destination_payment',
            'destination_project',
143
            #'destination_referral',
144
            'destination_section',
145 146 147
            #'destination_trade',
            #'destination_transport'
            )
148

149 150 151 152 153 154
  # Helper Methods
  def _getExplanationRelatedSimulationMovementValueList(self, explanation):
    explanation_cache = _getExplanationCache(explanation)
    return explanation_cache.getBusinessPathRelatedSimulationMovementValueList(self)

  # XXX-JPS UNkonwn ?
155 156
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getArrowCategoryDict')
157
  def getArrowCategoryDict(self, context=None, **kw): # XXX-JPS do we need it in API ?
158 159 160 161 162 163 164 165 166 167 168 169 170
    result = {}
    dynamic_category_list = self._getDynamicCategoryList(context)
    for base_category in self.getSourceArrowBaseCategoryList() +\
            self.getDestinationArrowBaseCategoryList():
      category_url_list = Path._getAcquiredCategoryMembershipList(
        self, base_category, **kw)
      if len(category_url_list) == 0 and context is not None:
        category_url_list = self._filterCategoryList(dynamic_category_list,
                                                     base_category, **kw)
      if len(category_url_list) > 0:
        result[base_category] = category_url_list
    return result

171
  # ICategoryAccessProvider overridden methods
172 173
  def _getCategoryMembershipList(self, category, **kw):
    """
174 175
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
176
    """
177
    context = kw.pop('context')
178
    result = Path._getCategoryMembershipList(self, category, **kw)
179 180
    if len(result) > 0:
      return result
181 182
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
183 184
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
185 186 187 188
    return result

  def _getAcquiredCategoryMembershipList(self, category, **kw):
    """
189 190
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
191
    """
192
    context = kw.pop('context', None)
193
    result = Path._getAcquiredCategoryMembershipList(self, category, **kw)
194 195
    if len(result) > 0:
      return result
196 197
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
198 199
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
200 201
    return result

202 203 204
  def _filterCategoryList(self, category_list, category, spec=(),
                          filter=None, portal_type=(), base=0,
                          keep_default=1, checked_permission=None):
205 206 207 208
    """
      XXX - implementation missing
      TBD - look at CategoryTool._buildFilter for inspiration
    """
209 210 211 212 213 214 215 216
    # 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)]
217 218 219

  # Dynamic context based categories
  def _getDynamicCategoryList(self, context):
220 221
    return self._getDynamicSourceCategoryList(context) \
         + self._getDynamicDestinationCategoryList(context)
222 223 224 225 226 227

  def _getDynamicSourceCategoryList(self, context):
    method_id = self.getSourceMethodId()
    if method_id:
      method = getattr(self, method_id)
      return method(context)
228
    return []
229 230 231 232 233 234

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

237 238 239 240 241 242 243 244 245 246 247 248 249 250
  # IBusinessPath implementation
  security.declareProtected(Permissions.AccessContentsInformation,
                                            'getMovementCompletionDate')
  def getMovementCompletionDate(self, movement):
    """Returns the date of completion of the movemnet 
    based on paremeters of the business path. This complete date can be
    the start date, the stop date, the date of a given workflow transition
    on the explaining delivery, etc.

    movement -- a Simulation Movement
    """
    method_id = self.getCompletionDateMethodId()
    method = getattr(movement, method_id) # We wish to raise if it does not exist
    return method()
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266

  def getCompletionDate(self, explanation):
    """Returns the date of completion of the movemnet 
    based on paremeters of the business path. This complete date can be
    the start date, the stop date, the date of a given workflow transition
    on the explaining delivery, etc.

    XXX - DOC

    movement -- a Simulation Movement
    """
    date_list = []
    for movement in self._getExplanationRelatedSimulationMovementValueList(explanation):
      date_list.append(self.getMovementCompletionDate(movement))

    return max(date_list)
267 268 269 270 271 272 273 274 275 276
  
  security.declareProtected(Permissions.AccessContentsInformation,
                                            'getExpectedQuantity')
  def getExpectedQuantity(self, amount):
    """Returns the new quantity for the provided amount taking
    into account the efficiency or the quantity defined on the business path.
    This is used to implement payment conditions or splitting production
    over multiple path. The total of getExpectedQuantity for all business
    path which are applicable should never exceed the original quantity.
    The implementation of this validation is left to rules.
277
    """
278 279 280 281 282 283
    if self.getQuantity():
      return self.getQuantity()
    elif self.getEfficiency():
      return amount.getQuantity() * self.getEfficiency()
    else:
      return amount.getQuantity()
284

285 286
  security.declareProtected(Permissions.AccessContentsInformation,
      'isCompleted')
287
  def isCompleted(self, explanation):
288 289 290 291 292 293 294 295 296 297 298 299 300 301
    """returns True if all related simulation movements for this explanation
    document are in a simulation state which is considered as completed
    according to the configuration of the current business path.
    Completed means that it is possible to move to next step
    of Business Process. This method does not check however whether previous
    trade states of a given business process are completed or not.
    Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.

    explanation -- the Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree and a union 
                   business process.

    NOTE: simulation movements can be completed (ex. in 'started' state) but
    not yet frozen (ex. in 'delivered' state).
302
    """
303
    acceptable_state_list = self.getCompletedStateList()
304 305
    for movement in self._getExplanationRelatedSimulationMovementValueList(
                                                                explanation):
306 307 308 309
      if movement.getSimulationState() not in acceptable_state_list:
        return False
    return True

310 311
  security.declareProtected(Permissions.AccessContentsInformation,
      'isPartiallyCompleted')
312
  def isPartiallyCompleted(self, explanation):
313 314 315 316 317 318 319 320 321 322 323
    """returns True if some related simulation movements for this explanation
    document are in a simulation state which is considered as completed
    according to the configuration of the current business path.
    Completed means that it is possible to move to next step
    of Business Process. This method does not check however whether previous
    trade states of a given business process are completed or not.
    Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.

    explanation -- the Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree and a union 
                   business process.
324
    """
325
    acceptable_state_list = self.getCompletedStateList()
326 327
    for movement in self._getExplanationRelatedSimulationMovementValueList(
                                                                explanation):
328 329 330 331
      if movement.getSimulationState() in acceptable_state_list:
        return True
    return False

332
  security.declareProtected(Permissions.AccessContentsInformation, 'isFrozen')
333
  def isFrozen(self, explanation):
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    """returns True if all related simulation movements for this explanation
    document are in a simulation state which is considered as frozen
    according to the configuration of the current business path.
    Frozen means that simulation movement cannot be modified.
    This method does not check however whether previous
    trade states of a given business process are completed or not.
    Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose.

    explanation -- the Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree and a union 
                   business process.

    NOTE: simulation movements can be frozen (ex. in 'stopped' state) but
    not yet completed (ex. in 'delivered' state).
    """
    acceptable_state_list = self.getFrozenStateList()
    movement_list = self._getExplanationRelatedSimulationMovementValueList(
                                                                explanation)
    if not movement_list:
      return False # Frozen is True only if some delivered movements exist
354
    for movement in movement_list:
355
      if movement.getDelivery() and movement.getSimulationState() not in acceptable_state_list: # XXX-JPS is it acceptable optimizatoin ?
356 357 358
        return False
    return True

359 360 361 362 363 364 365 366 367
  def isDelivered(self, explanation):
    """XXX
    """
    for simulation_movement in self._getExplanationRelatedSimulationMovementValueList(
        explanation):
      if not simulation_movement.getDelivery():
        return False
    return True

368 369 370
  def build(self, explanation):
    """Builds all related movements in the simulation using the builders
    defined on the Business Path.
371

372 373 374
    explanation -- the Order, Order Line, Delivery or Delivery Line which
                   implicitely defines a simulation subtree and a union 
                   business process.
375
    """
376 377 378 379 380 381 382 383 384
    builder_list = self.getDeliveryBuilderValueList()
    explanation_cache = _getExplanationCache(explanation)
    for builder in builder_list:
      # Call build on each builder
      # Provide 2 parameters: self and and explanation_cache
      builder.build(select_method_dict={
        'business_path': self,
        'explanation_cache': explanation_cache,
      })
385

386 387
  # XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX GARBAGE FROM HERE - all code will be removed
  # _LEGACY_ prefix is used to make sure nobody will call methods
388

389
  def _LEGACY_getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
390 391 392 393 394 395 396
    """
      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
    """
397 398 399 400 401 402 403
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStartDate,
                                 self._getPredecessorExpectedStartDate,
                                 self._getSuccessorExpectedStartDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

404
  def _LEGACY__getRootExplanationExpectedStartDate(self, explanation, *args, **kwargs):
405 406 407
    if self.getParentValue().isStartDateReferential():
      return explanation.getStartDate()
    else:
408 409 410
      expected_date = self.getExpectedStopDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
411

412
  def _LEGACY__getPredecessorExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
413
    if predecessor_date is None:
414 415
      node = self.getPredecessorValue()
      if node is not None:
416 417
        predecessor_date = self.getParentValue().getExpectedStateCompletionDate(
          explanation, node, *args, **kwargs)
418 419 420
    if predecessor_date is not None:
      return predecessor_date + self.getWaitTime()

421
  def _LEGACY__getSuccessorExpectedStartDate(self, explanation, *args, **kwargs):
422 423
    node = self.getSuccessorValue()
    if node is not None:
424 425
      expected_date =  self.getParentValue().getExpectedStateBeginningDate(
        explanation, node, *args, **kwargs)
426 427
      if expected_date is not None:
        return expected_date - self.getLeadTime()
428

429
  def _LEGACY_getExpectedStopDate(self, explanation, predecessor_date=None, *args, **kwargs):
430 431 432 433 434 435 436
    """
      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
    """
437 438 439 440 441 442 443
    return self._getExpectedDate(explanation,
                                 self._getRootExplanationExpectedStopDate,
                                 self._getPredecessorExpectedStopDate,
                                 self._getSuccessorExpectedStopDate,
                                 predecessor_date=predecessor_date,
                                 *args, **kwargs)

444
  def _LEGACY__getRootExplanationExpectedStopDate(self, explanation, *args, **kwargs):
445 446 447
    if self.getParentValue().isStopDateReferential():
      return explanation.getStopDate()
    else:
448 449 450
      expected_date = self.getExpectedStartDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getLeadTime()
451

452
  def _LEGACY__getPredecessorExpectedStopDate(self, explanation, *args, **kwargs):
453 454
    node = self.getPredecessorValue()
    if node is not None:
455 456
      expected_date = self.getParentValue().getExpectedStateCompletionDate(
         explanation, node, *args, **kwargs)
457 458
      if expected_date is not None:
        return expected_date + self.getWaitTime() + self.getLeadTime()
459

460
  def _LEGACY__getSuccessorExpectedStopDate(self, explanation, *args, **kwargs):
461 462
    node = self.getSuccessorValue()
    if node is not None:
463 464
      return self.getParentValue().getExpectedStateBeginningDate(
         explanation, node, *args, **kwargs)
465

466
  def _LEGACY__getExpectedDate(self, explanation, root_explanation_method,
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
                       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)

489 490
    predecessor_expected_date = predecessor_method(
      explanation, visited=visited, *args, **kwargs)
491

492 493
    successor_expected_date = successor_method(
      explanation, visited=visited, *args, **kwargs)
494 495 496 497 498 499 500 501 502 503 504 505 506

    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
507

508 509


510
  def _LEGACY__recurseGetValueList(self, document, portal_type):
511 512 513 514 515 516 517 518 519
    """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

520
  def _LEGACY__isMovementRelatedWithMovement(self, movement_value_a, movement_value_b): # XXX-JPS not in API
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
    """Checks if self is parent or children to movement_value

    This logic is Business Process specific for Simulation Movements, as
    sequence of Business Process is not related appearance of Simulation Tree

    movement_value_a, movement_value_b - movements to check relation between
    """
    movement_a_path = '%s/' % movement_value_a.getRelativeUrl()
    movement_b_path = '%s/' % movement_value_b.getRelativeUrl()

    if movement_a_path == movement_b_path or \
       movement_a_path.startswith(movement_b_path) or \
       movement_b_path.startswith(movement_a_path):
      return True
    return False

537
  def _LEGACY__isDeliverySimulationMovementRelated(self, simulation_movement,
538 539 540 541 542 543 544 545 546 547
                                           delivery_simulation_movement_list):
    """Helper method, which checks if simulation_movement is BPM like related
       with delivery"""
    for delivery_simulation_movement in delivery_simulation_movement_list:
      if self.isMovementRelatedWithMovement(delivery_simulation_movement,
          simulation_movement):
        return True
    return False

  # IBusinessPath implementation
548
  def _LEGACY__getRelatedSimulationMovementValueList(self, explanation): # XXX-JPS purpose ? NOT IN API
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
    """
      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
    """
    portal_catalog = self.getPortalObject().portal_catalog
    root_applied_rule_list = []

     
    if getattr(self, 'getMovementList', None) is None: # XXX-JPS temp hack
      return []

    delivery_simulation_movement_list = portal_catalog(
      delivery_uid=[x.getUid() for x in explanation.getMovementList()]) # XXX-JPS it seems explanation is not understood as it should - only the root

    for simulation_movement in delivery_simulation_movement_list:
      applied_rule = simulation_movement.getRootAppliedRule().getPath()
      if applied_rule not in root_applied_rule_list:
        root_applied_rule_list.append(applied_rule)

    simulation_movement_list = portal_catalog(
      portal_type='Simulation Movement', causality_uid=self.getUid(),
      path=['%s/%%' % x for x in root_applied_rule_list])

    return [simulation_movement.getObject() for simulation_movement
          in simulation_movement_list
          # related with explanation
          if self._isDeliverySimulationMovementRelated(
              simulation_movement, delivery_simulation_movement_list)]