BusinessPath.py 15.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 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#
# 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

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

import zope.interface

class BusinessPath(Path):
  """
    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 46 47
    BusinessPath are also used as helper to build deliveries from
    buildable movements. Here is the typical code of an alarm
    in charge of the building process.
48

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

      builder = portal_deliveries.default_order_builder
      for path in builder.getSpecialiseRelatedValueList() # or wharever category
        builder.build(causality_uid=path.getUid(),) # Select movemenents

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

61 62
    TODO:
      - finish build process implementation
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  """
  meta_type = 'ERP5 Business Path'
  portal_type = 'Business Path'
  isPredicate = 1

  # 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
                    , PropertySheet.BusinessPath
                    )

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

92
  # IArrowBase implementation
93 94 95 96 97 98
  security.declareProtected(Permissions.AccessContentsInformation, 'getSourceBaseCategoryList')
  def getSourceBaseCategoryList(self):
    """
      Returns all categories which are used to define the source
      of this Arrow
    """
99
    # Naive implementation - we must use category groups instead - XXX
100 101 102
    return ('source', 'source_section', 'source_payment', 'source_project',
        'source_administration', 'source_project', 'source_function',
        'source_payment', 'source_account')
103 104 105 106 107 108 109

  security.declareProtected(Permissions.AccessContentsInformation, 'getDestinationBaseCategoryList')
  def getDestinationBaseCategoryList(self):
    """
      Returns all categories which are used to define the destination
      of this Arrow
    """
110
    # Naive implementation - we must use category groups instead - XXX
111 112 113
    return ('destination', 'destination_section', 'destination_payment', 'destination_project',
        'destination_administration', 'destination_project', 'destination_function',
        'destination_payment', 'destination_account')
114 115 116 117

  # ICategoryAccessProvider overriden methods
  def _getCategoryMembershipList(self, category, **kw):
    """
118 119
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
120
    """
121
    context = kw.pop('context')
122
    result = Path._getCategoryMembershipList(self, category, **kw)
123 124
    if len(result) > 0:
      return result
125 126
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
127 128
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
129 130 131 132
    return result

  def _getAcquiredCategoryMembershipList(self, category, **kw):
    """
133 134
      Overridden in order to take into account dynamic arrow categories in case if no static
      categories are set on Business Path
135
    """
136
    context = kw.pop('context', None)
137
    result = Path._getAcquiredCategoryMembershipList(self, category, **kw)
138 139
    if len(result) > 0:
      return result
140 141
    if context is not None:
      dynamic_category_list = self._getDynamicCategoryList(context)
142 143
      dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw)
      result = dynamic_category_list
144 145 146 147 148 149 150 151
    return result

  def _filterCategoryList(self, category_list, category, spec=(), filter=None, portal_type=(), base=0, 
                         keep_default=1, checked_permission=None):
    """
      XXX - implementation missing
      TBD - look at CategoryTool._buildFilter for inspiration
    """
152 153 154 155 156 157 158 159
    # 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)]
160 161 162

  # Dynamic context based categories
  def _getDynamicCategoryList(self, context):
163 164
    return self._getDynamicSourceCategoryList(context) \
         + self._getDynamicDestinationCategoryList(context)
165 166 167 168 169 170

  def _getDynamicSourceCategoryList(self, context):
    method_id = self.getSourceMethodId()
    if method_id:
      method = getattr(self, method_id)
      return method(context)
171
    return []
172 173 174 175 176 177

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

180
  # IBusinessBuildable implementation
181
  def isBuildable(self, explanation):
182 183
    """
    """
184 185 186 187 188 189 190 191 192
    # 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
    for simulation_movement in self._getRelatedSimulationMovementValueList(
        explanation):
      if simulation_movement.getDeliveryValue() is None:
        result = True
193 194
    predecessor = self.getPredecessorValue()
    if predecessor is None:
195
      return result
196
    if predecessor.isCompleted(explanation):
197
      return result
198 199 200 201 202 203 204
    return False

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

205 206 207 208 209 210 211 212 213 214 215 216
  def _getExplanationUidList(self, explanation):
    """Helper method to fetch really explanation related movements

       As Business Path is related to movement by causality, thanks to
       trade_phase during expand, it is correct to pass too much explanations
       than not enough"""
    explanation_uid_list = [explanation.getUid()]
    for ex in explanation.getCausalityRelatedValueList(
        portal_type=self.getPortalDeliveryTypeList()):
      explanation_uid_list.extend(self._getExplanationUidList(ex))
    return explanation_uid_list

217
  def build(self, explanation):
218 219 220
    """
      Build
    """
221
    builder_list = self.getDeliveryBuilderValueList() # Missing method
222
    for builder in builder_list:
223 224 225 226 227 228 229
      # 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)
      })
230

231
  def _getRelatedSimulationMovementValueList(self, explanation): # XXX - What API ?
232
    """
233
      Returns all Simulation Movements related to explanation
234
    """
235 236 237 238 239 240
    # XXX What about explanations for causality related documents to explanation?
    explanation_uid_list = self._getExplanationUidList(explanation)
    # getCausalityRelated do not support filtering, so post filtering needed
    return [x for x in self.getCausalityRelatedValueList(
      portal_type='Simulation Movement')
      if x.getExplanationUid() in explanation_uid_list]
241

242
  # IBusinessCompletable implementation
243
  def isCompleted(self, explanation):
244 245 246 247
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
248
    acceptable_state_list = self.getCompletedStateList()
249
    for movement in self._getRelatedSimulationMovementValueList(explanation):
250 251 252 253 254
      if movement.getSimulationState() not in acceptable_state_list:
        return False
    return True

  def isPartiallyCompleted(self, explanation):
255 256 257 258
    """
      Looks at all simulation related movements
      and checks the simulation_state of the delivery
    """
259
    acceptable_state_list = self.getCompletedStateList()
260
    for movement in self._getRelatedSimulationMovementValueList(explanation):
261 262 263 264 265
      if movement.getSimulationState() in acceptable_state_list:
        return True
    return False

  def isFrozen(self, explanation):
266 267 268 269
    """
      Looks at all simulation related movements
      and checks if frozen
    """
270
    movement_list = self._getRelatedSimulationMovementValueList(explanation)
271 272 273 274 275 276 277
    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

278
  # IBusinessPath implementation
279
  def getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
280 281 282 283 284 285 286
    """
      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
    """
287 288 289 290 291 292 293 294 295 296 297
    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:
298 299 300
      expected_date = self.getExpectedStopDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
301 302

  def _getPredecessorExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs):
303
    if predecessor_date is None:
304 305 306 307 308 309 310 311 312
      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:
313 314 315
      expected_date =  node.getExpectedBeginningDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date - self.getLeadTime()
316 317

  def getExpectedStopDate(self, explanation, predecessor_date=None, *args, **kwargs):
318 319 320 321 322 323 324
    """
      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
    """
325 326 327 328 329 330 331 332 333 334 335
    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:
336 337 338
      expected_date = self.getExpectedStartDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getLeadTime()
339 340 341 342

  def _getPredecessorExpectedStopDate(self, explanation, *args, **kwargs):
    node = self.getPredecessorValue()
    if node is not None:
343 344 345
      expected_date = node.getExpectedCompletionDate(explanation, *args, **kwargs)
      if expected_date is not None:
        return expected_date + self.getWaitTime() + self.getLeadTime()
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374

  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)

375 376
    predecessor_expected_date = predecessor_method(
      explanation, visited=visited, *args, **kwargs)
377

378 379
    successor_expected_date = successor_method(
      explanation, visited=visited, *args, **kwargs)
380 381 382 383 384 385 386 387 388 389 390 391 392

    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