ExplanationCache.py 17 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2O10 Nexedi SA and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
#
# 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.
#
##############################################################################

30
from collections import defaultdict
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31
from zLOG import LOG
32
from Products.ERP5Type.Cache import transactional_cached
33
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
34

35 36 37 38 39 40 41

class treenode(defaultdict):

  def __init__(self):
    defaultdict.__init__(self, self.__class__)


42 43 44 45 46 47
class ExplanationCache:
  """ExplanationCache provides a central access to 
  all parameters and values which are needed to process 
  an explanation. It is based on the idea that a value is calculated
  once and once only, as a way to accelerate performance of algorithms
  related to an explanation.
48 49 50 51

  TODO: 
  - implement property explanation calculation
    (with parent simulation movements, not only children)
52
  """
53 54

  def __init__(self, explanation):
55 56 57
    """Explanation cache keeps a handful list of caches
    to accelerate performance of business path browsing and
    business process algorithms
58 59 60
    """
    # Define share properties
    self.explanation = explanation
61
    self.portal_catalog = explanation.getPortalObject().portal_catalog
62 63 64
    self.simulation_movement_cache = {} # Simulation Movement Cache
    self.explanation_uid_cache = []
    self.explanation_path_pattern_cache = []
65
    self.reference_date_cache = {}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
66
    self.closure_cache = {}
67
    self.union_cache = None
68

69
  def _getDeliveryMovementList(self):
70 71
    """Returns self if explanation is a delivery line
    or the list of explanation delivery lines if explanation
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
    is a delivery
    """
    explanation = self.explanation
    if explanation.isDelivery():
      # Gather all movements of the delivery
      delivery_movement_list = explanation.getMovementList()
    else:
      # Only consider a single movement - XXX-JPS is this OK when we have lines in lines ?
      delivery_movement_list = [explanation]
    return delivery_movement_list

  def getRootExplanationUidList(self):
    """Return the list of explanation_uid values involved
    in the context of the explanation. This will be useful later
    in order to accelerate searches in the catalog.
    """
    # Return cache if defined
    if self.explanation_uid_cache:
      return self.explanation_uid_cache
    result = set()
    # For each delivery movement
93
    for movement in self._getDeliveryMovementList():
94 95 96 97
      # For each simulation movement
      for simulation_movement in movement.getDeliveryRelatedValueList():
        result.add(simulation_movement.getExplanationUid()) # XXX-JPS use new API later
    # Return result
98 99
    self.explanation_uid_cache = tuple(result)
    return self.explanation_uid_cache
100 101 102 103 104

  def getSimulationPathPatternList(self):
    """Return the list of root path of simulation tree which are 
    involved in the context of the explanation. This will be useful later
    in order to accelerate searches in the catalog.
105 106 107 108 109 110

    XXX-JPS: QUESTION: should we consider only patterns starting from
    the movement, or from the root delivery line related movement ?
    In one case, we must provided appropriate explanation for everything
    to work. In the other case, we can take any delivery line any where
    a explanation.
111 112
    """
    # Return cache if defined
113 114 115
    result = self.explanation_path_pattern_cache
    if not result:
      prefix = self.portal_catalog.getPortalObject().getPath() + '/'
116

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
      # path_dict is typically like this:
      #  {'portal_simulation': {'3': {'4': None}}}
      path_dict = treenode()
      simulation_movement_list = []
      for movement in self._getDeliveryMovementList():
        simulation_movement_list += movement.getDeliveryRelatedList()
      simulation_movement_list.sort()
      # Now it's sorted, we'll process parents before their children.
      for simulation_movement in simulation_movement_list:
        local_path_dict = path_dict
        container_path = simulation_movement.split('/')
        simulation_movement_id = container_path.pop()
        for path_id in container_path:
          local_path_dict = local_path_dict[path_id]
          if local_path_dict is None:
            break # A movement was already inserted
        else:
134
          # We have a real root
135 136 137 138
          local_path_dict[simulation_movement_id] = None
          simulation_movement = prefix + simulation_movement
          result.append(simulation_movement)
          result.append(simulation_movement.replace('_', r'\_')  + '/%')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
139
          # XXX-JPS here we must add all parent movements XXX-JPS
140 141
    return result

142 143
  def getBusinessLinkRelatedSimulationMovementValueList(self, business_link):
    """Returns the list of simulation movements caused by a business_link
144 145
    in the context the our explanation.
    """
146
    return self.getSimulationMovementValueList(causality_uid=business_link.getUid())
147
    
148 149
  def getBusinessLinkRelatedMovementValueList(self, business_link):
    """Returns the list of delivery movements related to a business_link
Jean-Paul Smets's avatar
Jean-Paul Smets committed
150 151 152
    in the context the our explanation.
    """
    #XXXXXXXXXXX BAD
153
    return self.getSimulationMovementValueList(causality_uid=business_link.getUid())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
154

155
  @UnrestrictedMethod
156
  def getSimulationMovementValueList(self, **kw):
157 158 159
    """Search Simulation Movements related to our explanation.
    Cache result so that the second time we saarch for the same
    list we need not involve the catalog again.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
160 161 162 163 164

    NOTE:
    - this method can be made catalog independent
      in case explanation is an applied rule, we can
      browse parent and child movement
165 166
    """
    kw_tuple = tuple(kw.items()) # We hope that no sorting is needed
167

168
    def getParentSimulationMovementValueList(obj, movement_list, trade_phase):
Julien Muchembled's avatar
Julien Muchembled committed
169 170
      parent = obj.getParentValue()
      while parent.getPortalType() == "Simulation Movement":
171
        if parent.isMemberOf(trade_phase, strict_membership=1):
Julien Muchembled's avatar
Julien Muchembled committed
172 173
          movement_list.append(parent)
        parent = parent.getParentValue().getParentValue()
174

175
    def getChildSimulationMovementValueList(obj, movement_list, trade_phase):
Julien Muchembled's avatar
Julien Muchembled committed
176 177
      for child in obj.objectValues():
        if (child.getPortalType() == "Simulation Movement" and
178
            child.isMemberOf(trade_phase, strict_membership=1)):
179
          movement_list.append(child)
Aurel's avatar
Aurel committed
180
        getChildSimulationMovementValueList(child, movement_list, trade_phase)
181

Julien Muchembled's avatar
Julien Muchembled committed
182
    if kw_tuple not in self.simulation_movement_cache:
183 184
      if self.explanation.getPortalType() == "Applied Rule":
        movement_list = []
Julien Muchembled's avatar
Julien Muchembled committed
185 186
        getParentSimulationMovementValueList(self.explanation, movement_list, kw['trade_phase'])
        getChildSimulationMovementValueList(self.explanation, movement_list, kw['trade_phase'])
187 188
        self.simulation_movement_cache[kw_tuple] = movement_list
      else:
189 190
        # XXX-Aurel : the following code seems not working as expected, it returns
        # all simulation movements from a site
191 192
        if kw.get('path', None) is None:
          kw['path'] = self.getSimulationPathPatternList() # XXX-JPS Explicit Query is better
193 194 195 196 197 198 199 200 201 202
        # XXX-Seb It seems incompatible with the way explanation is working
        # Indeed, the explanation is not the same all other the simulation tree
        #      path                explanation
        # portal_simulation/91/1 testing_folder/17
        # portal_simulation/91/1/1 testing_folder/17
        # portal_simulation/91/1/1/1 testing_folder/18
        # portal_simulation/91/1/1/1/1 testing_folder/18
        # portal_simulation/91/1/1/1/1/1 testing_folder/17
        #if kw.get('explanation_uid', None) is None:
        #  kw['explanation_uid'] = self.getRootExplanationUidList()
203 204 205 206
        if 'trade_phase' in kw:
          kw['trade_phase_relative_url'] = kw.pop('trade_phase')
        self.simulation_movement_cache[kw_tuple] = self.portal_catalog(
          portal_type="Simulation Movement", **kw)
207
        
208 209
    return self.simulation_movement_cache[kw_tuple]

210
  def getBusinessLinkValueList(self, **kw):
211 212 213
    """Find all business path which are related to the simulation 
    trees defined by the explanation.
    """
214
    business_type_list = self.getPortalBusinessLinkTypeList()
215 216 217
    simulation_movement_list = self.getSimulationMovementValueList()
    simulation_movement_uid_list = map(lambda x:x.uid, simulation_movement_list) 
    # We could use related keys instead of 2 queries
218
    business_link_list = self.portal_catalog(
219 220 221
                      portal_type=business_type_list,
                      causality_related_uid=simulation_movement_uid_list,
                      **kw)
222
    return business_link_list
223

224 225
  def getBusinessLinkClosure(self, business_process, business_link):
    """Creates a Business Process by filtering out all Business Link
226
    in 'business_process' which are not related to a simulation movement
Jean-Paul Smets's avatar
Jean-Paul Smets committed
227
    which is either a parent or a child of explanation simulations movements
228
    caused by 'business_link'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
229

230
    NOTE: Business Link Closure must be at least as "big" as composed 
Jean-Paul Smets's avatar
Jean-Paul Smets committed
231 232
    business path. The appropriate calculation is still not clear. 
    Options are:
233 234
      - take all link of composed business link (even not yet expanded)
      - take all link of composed business link which phase is not yet expanded
235
    """
236
    # Try to return cached value first
237
    new_business_process = self.closure_cache.get(business_link, None)
238 239 240
    if new_business_process is not None:
      return new_business_process

241
    # Build a list of path patterns which apply to current business_link
242 243 244 245 246
    path_list = iter(self.getSimulationPathPatternList())
    path_dict = dict((x, path_list.next()) for x in path_list)
    # path_dict is like this;
    # {'/erp5/portal_simulation/3/4': r'/erp5/portal\_simulation/3/4/%'}
    path_list = []
Julien Muchembled's avatar
style  
Julien Muchembled committed
247 248
    for simulation_movement in \
        self.getBusinessLinkRelatedSimulationMovementValueList(business_link):
249
      simulation_path = simulation_movement.getPath()
250 251 252 253 254 255 256 257 258 259 260 261
      if simulation_path in path_dict:
        path = simulation_path
      else:
        for path in path_dict:
          if simulation_path.startswith(path) and \
             simulation_path[len(path)] == '/':
            break
        else:
          continue
      # Only keep a path pattern which matches current simulation movement
      path_list.append(path)
      path_list.append(path_dict.pop(path))
262

263 264 265 266
    # Lookup in cache based on path_list
    path_list.sort()
    path_list = tuple(path_list)
    new_business_process = self.closure_cache.get(path_list)
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
    if new_business_process is None:
      business_link_list = []
      new_business_process = business_process
      for x in business_process.getBusinessLinkValueList():
        if self.getSimulationMovementValueList(path=path_list,
                                               causality_uid=x.getUid()):
          # We have matching movements.
          business_link_list.append(x)
        else:
          new_business_process = None
      if new_business_process is None:
        # Build a new closure business process.
        # Initially, business_process is often the result of
        # asComposedDocument() and business_process.getParentValue() is not a
        # module where newContent() allows creation of Business Processes.
        # XXX-JPS is this really OK with union business processes
        from Products.ERP5Type.Document import newTempBusinessProcess
        new_business_process = newTempBusinessProcess(
          self.explanation, 'closure_business_process')
        for i, x in enumerate(business_link_list):
          id = 'closure_path_%s' % i
          new_business_process._setOb(id, x.asContext(id=id))
      self.closure_cache[path_list] = new_business_process
290

291
    self.closure_cache[business_link] = new_business_process
292 293 294
    return new_business_process

  def getUnionBusinessProcess(self):
295
    """Return a Business Process made of all Business Link
296 297 298 299 300 301 302 303 304 305 306 307
    which are the cause of Simulation Movements in the simulation
    trees related to explanation.
    """
    # Try to return cached value first
    new_business_process = self.union_cache
    if new_business_process is not None:
      return new_business_process

    # Build Union Business Process
    from Products.ERP5Type.Document import newTempBusinessProcess
    new_business_process = newTempBusinessProcess(self.explanation, 'union_business_process')
    i = 0
308
    for business_link in self.getBusinessLinkValueList():
309 310
      i += 1
      id = 'union_path_%s' % i
311
      new_business_process._setOb(id, business_link.asContext(id=id))
312

313 314
    # Keep it in cache and return
    self.union_cache = new_business_process
315 316
    return new_business_process

317 318
  def getReferenceDate(self, business_process, trade_phase,
                       reference_date_method_id, delay_mode=None):
319 320 321
    """Browse parent similation movements until a movement with
    appropriate trade_phase is found.
    """
322 323 324 325 326 327
    cache = self.reference_date_cache
    reference_date_key = (business_process.getPhysicalPath(), trade_phase,
                          reference_date_method_id, delay_mode)
    try:
      result = cache[reference_date_key]
      if result is self: # use self as marker to detect infinite recursion
328 329
        raise ValueError('No reference date is defined, probably due to missing Trade Model Path in Business Process')
      return result
330 331
    except KeyError:
      cache[reference_date_key] = self
332

333 334 335 336 337 338 339 340 341
    # Find simulation movements with appropriate trade_phase
    movement_list = self.getSimulationMovementValueList(trade_phase=trade_phase)

    # Case 1: some (parent) simulation movement with appropriate trade phase exists
    if len(movement_list):
      # XXX-JPS - for now take arbitrary one
      # but we should in reality some way to configure this
      movement = movement_list[0]
      method = getattr(movement, reference_date_method_id)
342
      cache[reference_date_key] = result = method()
343
      return result
344 345 346 347 348 349

    # Case 2: we must recursively find another trade phase
    # to find the value recursively
    # XXX-JPS this is only useful for production (MRP) in reality
    # whenever trade model path define time constraints within the same
    # movement generator (ie. transformation with multiple phases)
350
    path_list = business_process.getTradeModelPathValueList(trade_phase=trade_phase, context=business_process)
351 352 353
    if not len(path_list):
      raise ValueError('No Trade Model Path defines a reference data.')

354
    path = path_list[0]
355 356 357 358 359 360 361 362 363 364 365
    # XXX-JPS - for now take arbitrary one
    # but we should in reality some way to configure this
    start_date, stop_date = business_process.getExpectedTradeModelPathStartAndStopDate(
                                   self.explanation, path, delay_mode=delay_mode)

    # Create a fake simulation movement and lookup property
    movement = self.explanation.newContent(portal_type="Simulation Movement", 
                                           temp_object=True, 
                                           start_date=start_date, stop_date=stop_date,
                                           trade_phase=trade_phase, causality=path)
    method = getattr(movement, reference_date_method_id)
366
    cache[reference_date_key] = result = method()
367
    return result
368

369
_getExplanationCache = transactional_cached()(ExplanationCache)
370

371
def _getBusinessLinkClosure(business_process, explanation, business_link):
372
  """Returns a closure Business Process for given 
373 374
  business_link and explanation. This Business Process
  contains only those Business Link which are related to business_link
375 376
  in the context of explanation.
  """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
377 378 379 380 381
  if explanation.getPortalType() == "Applied Rule":
    # There is no way to guess the closure during expand
    # since some movements may not be generated. The resulting
    # closure might be smaller than expexted
    return business_process
382
  explanation_cache = _getExplanationCache(explanation)
383
  return explanation_cache.getBusinessLinkClosure(business_process, business_link)
384 385

def _getUnionBusinessProcess(explanation):
386
  """Build a Business Process by taking the union of Business Link
387
  which are involved in the simulation trees related to explanation
388 389
  """
  explanation_cache = _getExplanationCache(explanation)
390
  return explanation_cache.getUnionBusinessProcess()