Blame view

product/ERP5/Tool/SimulationTool.py 135 KB
Jean-Paul Smets committed
1 2
##############################################################################
#
Romain Courteaud committed
3
# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Romain Courteaud committed
5
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# 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.
#
##############################################################################

Jérome Perrin committed
30
from Products.CMFCore.utils import getToolByName
Jean-Paul Smets committed
31 32

from AccessControl import ClassSecurityInfo
33
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
Jean-Paul Smets committed
34
from Products.ERP5Type import Permissions
Sebastien Robin committed
35
from Products.ERP5Type.Tool.BaseTool import BaseTool
Sebastien Robin committed
36
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Jean-Paul Smets committed
37 38 39

from Products.ERP5 import _dtmldir

Aurel committed
40
from zLOG import LOG, PROBLEM, WARNING, INFO
Jean-Paul Smets committed
41 42

from Products.ERP5.Capacity.GLPK import solve
Julien Muchembled committed
43
from numpy import zeros, resize
Alexandre Boeglin committed
44
from DateTime import DateTime
Jean-Paul Smets committed
45

Romain Courteaud committed
46 47
from Products.ERP5 import DeliverySolver
from Products.ERP5 import TargetSolver
Romain Courteaud committed
48
from Products.PythonScripts.Utility import allow_class
Jean-Paul Smets committed
49

Vincent Pelletier committed
50
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery, SimpleQuery
Sebastien Robin committed
51

Vincent Pelletier committed
52
from Shared.DC.ZRDB.Results import Results
Julien Muchembled committed
53
from Products.ERP5Type.Utils import mergeZRDBResults
Aurel committed
54 55
from App.Extensions import getBrain
from MySQLdb import ProgrammingError
Sebastien Robin committed
56
from MySQLdb.constants.ER import NO_SUCH_TABLE
Vincent Pelletier committed
57

Aurel committed
58
from hashlib import md5
Nicolas Dumazet committed
59
from warnings import warn
Aurel committed
60 61 62 63 64 65 66
from cPickle import loads, dumps
from copy import deepcopy

MYSQL_MIN_DATETIME_RESOLUTION = 1/86400.

class StockOptimisationError(Exception):
    pass
Nicolas Dumazet committed
67

Romain Courteaud committed
68
class SimulationTool(BaseTool):
Jean-Paul Smets committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
    """
    The SimulationTool implements the ERP5
    simulation algorithmics.


    Examples of applications:

    -

    -
    ERP5 main purpose:

    -

    -

    """
    id = 'portal_simulation'
    meta_type = 'ERP5 Simulation Tool'
Jean-Paul Smets committed
88
    portal_type = 'Simulation Tool'
Jean-Paul Smets committed
89 90 91 92 93 94 95 96 97 98 99 100
    allowed_types = ( 'ERP5 Applied Rule', )

    # Declarative Security
    security = ClassSecurityInfo()

    #
    #   ZMI methods
    #
    security.declareProtected( Permissions.ManagePortal, 'manage_overview' )
    manage_overview = DTMLFile( 'explainSimulationTool', _dtmldir )

    def filtered_meta_types(self, user=None):
Jérome Perrin committed
101 102 103 104 105 106 107
      # Filters the list of available meta types.
      all = SimulationTool.inheritedAttribute('filtered_meta_types')(self)
      meta_types = []
      for meta_type in self.all_meta_types():
        if meta_type['name'] in self.allowed_types:
          meta_types.append(meta_type)
      return meta_types
Jean-Paul Smets committed
108

Jérome Perrin committed
109 110 111
    def tpValues(self) :
      """ show the content in the left pane of the ZMI """
      return self.objectValues()
Aurel committed
112

Jérome Perrin committed
113 114 115
    security.declarePrivate('manage_afterAdd')
    def manage_afterAdd(self, item, container) :
      """Init permissions right after creation.
Aurel committed
116

Jérome Perrin committed
117 118 119 120 121 122 123 124 125 126 127 128
      Permissions in simulation tool are simple:
       o Each member can access and create some content.
       o Only manager can view, because simulation can be seen as
         sensitive information.
      """
      item.manage_permission(Permissions.AddPortalContent,
            ['Member', 'Author', 'Manager'])
      item.manage_permission(Permissions.AccessContentsInformation,
            ['Member', 'Auditor', 'Manager'])
      item.manage_permission(Permissions.View,
            ['Manager',])
      BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)
Jérome Perrin committed
129

Kazuhiko Shiozaki committed
130 131
    security.declareProtected(Permissions.AccessContentsInformation,
                              'solveDelivery')
Łukasz Nowak committed
132 133
    def solveDelivery(self, delivery, delivery_solver_name, target_solver_name,
                      additional_parameters=None, **kw):
Sebastien Robin committed
134
      """
Kazuhiko Shiozaki committed
135 136
        XXX obsoleted API

Łukasz Nowak committed
137
        Solves a delivery by calling first DeliverySolver, then TargetSolver
Sebastien Robin committed
138
      """
Łukasz Nowak committed
139 140 141
      return self._solveMovementOrDelivery(delivery, delivery_solver_name,
          target_solver_name, delivery=1,
          additional_parameters=additional_parameters, **kw)
Aurel committed
142

Kazuhiko Shiozaki committed
143 144
    security.declareProtected(Permissions.AccessContentsInformation,
                              'solveMovement')
Łukasz Nowak committed
145 146
    def solveMovement(self, movement, delivery_solver_name, target_solver_name,
                      additional_parameters=None, **kw):
Sebastien Robin committed
147
      """
Kazuhiko Shiozaki committed
148 149
        XXX obsoleted API

Łukasz Nowak committed
150
        Solves a movement by calling first DeliverySolver, then TargetSolver
Sebastien Robin committed
151
      """
Łukasz Nowak committed
152 153 154
      return self._solveMovementOrDelivery(movement, delivery_solver_name,
          target_solver_name, movement=1,
          additional_parameters=additional_parameters, **kw)
Aurel committed
155

Łukasz Nowak committed
156 157 158
    def _solveMovementOrDelivery(self, document, delivery_solver_name,
                                 target_solver_name, movement=0, delivery=0,
                                 additional_parameters=None,**kw):
Jean-Paul Smets committed
159
      """
Łukasz Nowak committed
160
        Solves a document by calling first DeliverySolver, then TargetSolver
Jean-Paul Smets committed
161
      """
Łukasz Nowak committed
162 163 164 165 166 167 168 169
      if movement == delivery:
        raise ValueError('Parameters movement and delivery have to be'
                         ' different')

      solve_result = []
      for solver_name, solver_module in ((delivery_solver_name, DeliverySolver),
                                         (target_solver_name, TargetSolver)):
        result = None
Romain Courteaud committed
170 171 172 173 174 175
        if solver_name is not None:
          solver_file_path = "%s.%s" % (solver_module.__name__,
                                        solver_name)
          __import__(solver_file_path)
          solver_file = getattr(solver_module, solver_name)
          solver_class = getattr(solver_file, solver_name)
Łukasz Nowak committed
176 177
          solver = solver_class(additional_parameters=additional_parameters,
              **kw)
Romain Courteaud committed
178

Sebastien Robin committed
179
          if movement:
Łukasz Nowak committed
180
            result = solver.solveMovement(document)
Sebastien Robin committed
181
          if delivery:
Łukasz Nowak committed
182 183 184
            result = solver.solveDelivery(document)
        solve_result.append(result)
      return solve_result
Aurel committed
185

Jean-Paul Smets committed
186 187
    #######################################################
    # Stock Management
Alexandre Boeglin committed
188

Jérome Perrin committed
189
    def _generatePropertyUidList(self, prop, as_text=0):
Alexandre Boeglin committed
190 191 192 193 194 195 196
      """
      converts relative_url or text (single element or list or dict)
        to an object usable by buildSQLQuery

      as_text == 0: tries to lookup an uid from the relative_url
      as_text == 1: directly passes the argument as text
      """
Jérome Perrin committed
197
      if prop is None :
Jérome Perrin committed
198 199
        return []
      category_tool = getToolByName(self, 'portal_categories')
Alexandre Boeglin committed
200
      property_uid_list = []
Jérome Perrin committed
201
      if isinstance(prop, str):
Jérome Perrin committed
202
        if not as_text:
Jérome Perrin committed
203
          prop_value = category_tool.getCategoryValue(prop)
Jérome Perrin committed
204
          if prop_value is None:
Jérome Perrin committed
205
            raise ValueError, 'Category %s does not exists' % prop
Jérome Perrin committed
206
          property_uid_list.append(prop_value.getUid())
Alexandre Boeglin committed
207
        else:
Jérome Perrin committed
208 209 210
          property_uid_list.append(prop)
      elif isinstance(prop, (list, tuple)):
        for property_item in prop :
Jérome Perrin committed
211
          if not as_text:
Jérome Perrin committed
212 213 214 215
            prop_value = category_tool.getCategoryValue(property_item)
            if prop_value is None:
              raise ValueError, 'Category %s does not exists' % property_item
            property_uid_list.append(prop_value.getUid())
Alexandre Boeglin committed
216 217
          else:
            property_uid_list.append(property_item)
Jérome Perrin committed
218
      elif isinstance(prop, dict):
Alexandre Boeglin committed
219
        tmp_uid_list = []
Jérome Perrin committed
220 221 222
        if isinstance(prop['query'], str):
          prop['query'] = [prop['query']]
        for property_item in prop['query'] :
Jérome Perrin committed
223
          if not as_text:
Jérome Perrin committed
224 225 226 227
            prop_value = category_tool.getCategoryValue(property_item)
            if prop_value is None:
              raise ValueError, 'Category %s does not exists' % property_item
            tmp_uid_list.append(prop_value.getUid())
Alexandre Boeglin committed
228 229
          else:
            tmp_uid_list.append(property_item)
Jérome Perrin committed
230
        if tmp_uid_list:
Alexandre Boeglin committed
231
          property_uid_list = {}
Jérome Perrin committed
232
          property_uid_list['operator'] = prop['operator']
Alexandre Boeglin committed
233 234 235
          property_uid_list['query'] = tmp_uid_list
      return property_uid_list

Vincent Pelletier committed
236 237
    def _getSimulationStateQuery(self, **kw):
      simulation_state_dict = self._getSimulationStateDict(**kw)
Vincent Pelletier committed
238
      return self._buildSimulationStateQuery(simulation_state_dict=simulation_state_dict)
Vincent Pelletier committed
239

Vincent Pelletier committed
240
    def _buildSimulationStateQuery(self, simulation_state_dict, table='stock'):
Vincent Pelletier committed
241 242
      simulation_state = simulation_state_dict.get('simulation_state')
      if simulation_state is not None:
Vincent Pelletier committed
243 244 245 246 247 248
        return SimpleQuery(**{table + '.simulation_state': simulation_state})
      input_simulation_state = simulation_state_dict.get('input_simulation_state')
      if input_simulation_state is not None:
        simulation_query = ComplexQuery(
          self._getIncreaseQuery(table, 'quantity', True),
          SimpleQuery(**{table + '.simulation_state': input_simulation_state}),
Vincent Pelletier committed
249
          logical_operator='AND',
Vincent Pelletier committed
250 251
        )
        output_simulation_state = simulation_state_dict.get('output_simulation_state')
Vincent Pelletier committed
252
        if output_simulation_state is not None:
Vincent Pelletier committed
253 254 255 256 257
          simulation_query = ComplexQuery(
            simulation_query,
            ComplexQuery(
              self._getIncreaseQuery(table, 'quantity', False),
              SimpleQuery(**{table + '.simulation_state': output_simulation_state}),
Vincent Pelletier committed
258
              logical_operator='AND',
Vincent Pelletier committed
259
            ),
Vincent Pelletier committed
260
            logical_operator='OR'
Vincent Pelletier committed
261 262
          )
        return simulation_query
Vincent Pelletier committed
263 264

    def _getSimulationStateDict(self, simulation_state=None, omit_transit=0,
Sebastien Robin committed
265 266 267 268 269 270 271 272 273 274 275 276
                                input_simulation_state=None,
                                output_simulation_state=None,
                                transit_simulation_state=None,
                                strict_simulation_state=None):
      """
      This method is used in order to give what should be
      the input_simulation_state or output_simulation_state
      depending on many parameters
      """
      string_or_list = (str, list, tuple)
      # Simulation States
      # If strict_simulation_state is set, we directly put it into the dictionary
Vincent Pelletier committed
277
      simulation_dict = {}
Sebastien Robin committed
278 279 280
      if strict_simulation_state:
        if isinstance(simulation_state, string_or_list)\
                and simulation_state:
Vincent Pelletier committed
281
           simulation_query = SimpleQuery(
Romain Courteaud committed
282
                   **{'stock.simulation_state': simulation_state})
Sebastien Robin committed
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 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
      else:
        # first, we evaluate simulation_state
        sql_kw = {}
        if simulation_state and isinstance(simulation_state, string_or_list):
          if isinstance(simulation_state, str):
            sql_kw['input_simulation_state'] = [simulation_state]
            sql_kw['output_simulation_state'] = [simulation_state]
          else:
            sql_kw['input_simulation_state'] = simulation_state
            sql_kw['output_simulation_state'] = simulation_state
        # then, if omit_transit == 1, we evaluate (simulation_state -
        # transit_simulation_state) for input_simulation_state
        if omit_transit:
          if isinstance(simulation_state, string_or_list)\
                and simulation_state:
            if isinstance(transit_simulation_state, string_or_list)\
                  and transit_simulation_state:
              # when we know both are usable, we try to calculate
              # (simulation_state - transit_simulation_state)
              if isinstance(simulation_state, str):
                simulation_state = [simulation_state]
              if isinstance(transit_simulation_state, str) :
                transit_simulation_state = [transit_simulation_state]
              delivered_simulation_state_list = []
              for state in simulation_state :
                if state not in transit_simulation_state :
                  delivered_simulation_state_list.append(state)
              sql_kw['input_simulation_state'] = delivered_simulation_state_list

        # alternatively, the user can directly define input_simulation_state
        # and output_simulation_state
        if input_simulation_state and isinstance(input_simulation_state,
                                                  string_or_list):
          if isinstance(input_simulation_state, str):
            input_simulation_state = [input_simulation_state]
          sql_kw['input_simulation_state'] = input_simulation_state
        if output_simulation_state and isinstance(output_simulation_state,
                                                  string_or_list):
          if isinstance(output_simulation_state, str):
            output_simulation_state = [output_simulation_state]
          sql_kw['output_simulation_state'] = output_simulation_state
        # XXX In this case, we must not set sql_kw[input_simumlation_state] before
        input_simulation_state = None
        output_simulation_state = None
        if sql_kw.has_key('input_simulation_state'):
          input_simulation_state = sql_kw.get('input_simulation_state')
        if sql_kw.has_key('output_simulation_state'):
          output_simulation_state = sql_kw.get('output_simulation_state')
        if input_simulation_state is not None \
           or output_simulation_state is not None:
          sql_kw.pop('input_simulation_state',None)
          sql_kw.pop('output_simulation_state',None)
        if input_simulation_state is not None:
          if output_simulation_state is not None:
            if input_simulation_state == output_simulation_state:
Vincent Pelletier committed
338
              simulation_dict['simulation_state'] = input_simulation_state
Sebastien Robin committed
339
            else:
Vincent Pelletier committed
340 341
              simulation_dict['input_simulation_state'] = input_simulation_state
              simulation_dict['output_simulation_state'] = output_simulation_state
Sebastien Robin committed
342
          else:
Vincent Pelletier committed
343
            simulation_dict['input_simulation_state'] = input_simulation_state
Sebastien Robin committed
344
        elif output_simulation_state is not None:
Vincent Pelletier committed
345 346
          simulation_dict['simulation_state'] = output_simulation_state
      return simulation_dict
Sebastien Robin committed
347

Vincent Pelletier committed
348
    def _getIncreaseQuery(self, table, column, increase, sql_catalog_id=None):
Sebastien Robin committed
349
      """
Vincent Pelletier committed
350 351 352 353 354 355 356 357 358 359 360 361 362 363
      Returns a Query filtering rows depending on whether they represent an
      increase or a decrease.
      table (string)
        Name of table to use as stock table.
      column (string)
        Name of interesting column. Supported values are:
        - total_price for asset price increase/decrease
        - quantity for quantity increase (aka input)/decrease (aka output)
      increase (bool)
        False: decreasing rows are kept
        True: increasing rows are kept
      sql_catalog_id (string or None)
        Idenfitier of an SQLCatalog object relevant to table, or None for
        default one.
Sebastien Robin committed
364
      """
Vincent Pelletier committed
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
      if column == 'total_price':
        dedicated_column = 'is_asset_increase'
      elif column == 'quantity':
        dedicated_column = 'is_input'
      else:
        raise ValueError('Unknown column %r' % (column, ))
      if self.getPortalObject().portal_catalog.hasColumn(
            dedicated_column,
            sql_catalog_id,
          ):
        return SimpleQuery(**{dedicated_column: increase})
      # Dedicated columns are not present, compute on the fly.
      return ComplexQuery(
        ComplexQuery(
          SimpleQuery(comparison_operator='<', **{table + '.' + column: 0}),
          SimpleQuery(**{table + '.is_cancellation': increase}),
Vincent Pelletier committed
381
          logical_operator='AND',
Vincent Pelletier committed
382 383 384 385
        ),
        ComplexQuery(
          SimpleQuery(comparison_operator='>=', **{table + '.' + column: 0}),
          SimpleQuery(**{table + '.is_cancellation': not increase}),
Vincent Pelletier committed
386
          logical_operator='AND',
Vincent Pelletier committed
387
        ),
Vincent Pelletier committed
388
        logical_operator='OR',
Vincent Pelletier committed
389
      )
Vincent Pelletier committed
390

Vincent Pelletier committed
391
    def _generateSQLKeywordDict(self, table='stock', **kw):
Vincent Pelletier committed
392
        sql_kw, new_kw = self._generateKeywordDict(**kw)
Vincent Pelletier committed
393 394
        return self._generateSQLKeywordDictFromKeywordDict(table=table,
                 sql_kw=sql_kw, new_kw=new_kw)
Vincent Pelletier committed
395

Vincent Pelletier committed
396 397
    def _generateSQLKeywordDictFromKeywordDict(self, table='stock', sql_kw={},
                                               new_kw={}):
Jérome Perrin committed
398
        ctool = getToolByName(self, 'portal_catalog')
Vincent Pelletier committed
399 400
        sql_kw = sql_kw.copy()
        new_kw = new_kw.copy()
Jérome Perrin committed
401

Jérome Perrin committed
402
        # Group-by expression  (eg. group_by=['node_uid'])
Kazuhiko Shiozaki committed
403
        group_by = new_kw.pop('group_by_list', [])
Nicolas Dumazet committed
404

Jérome Perrin committed
405
        # group by from stock table (eg. group_by_node=True)
Jérome Perrin committed
406
        # prepend table name to avoid ambiguities.
Jérome Perrin committed
407
        column_group_by = new_kw.pop('column_group_by', [])
Jérome Perrin committed
408 409
        if column_group_by:
          group_by.extend(['%s.%s' % (table, x) for x in column_group_by])
Jérome Perrin committed
410 411

        # group by from related keys columns (eg. group_by_node_category=True)
Jérome Perrin committed
412 413 414
        related_key_group_by = new_kw.pop('related_key_group_by', [])
        if related_key_group_by:
          group_by.extend(['%s_%s' % (table, x) for x in related_key_group_by])
Vincent Pelletier committed
415

Jérome Perrin committed
416 417
        # group by involving a related key (eg. group_by=['product_line_uid'])
        related_key_dict_passthrough_group_by = new_kw.get(
Kazuhiko Shiozaki committed
418
                'related_key_dict_passthrough', {}).pop('group_by_list', [])
Jérome Perrin committed
419 420 421
        if isinstance(related_key_dict_passthrough_group_by, basestring):
          related_key_dict_passthrough_group_by = (
                related_key_dict_passthrough_group_by,)
Jérome Perrin committed
422 423
        group_by.extend(related_key_dict_passthrough_group_by)

Jérome Perrin committed
424
        if group_by:
Kazuhiko Shiozaki committed
425
          new_kw['group_by_list'] = group_by
Jérome Perrin committed
426

Jérome Perrin committed
427
        # select expression
Julien Muchembled committed
428
        select_dict = new_kw.setdefault('select_dict', {})
Jérome Perrin committed
429 430
        related_key_select_expression_list = new_kw.pop(
                'related_key_select_expression_list', [])
Jérome Perrin committed
431 432 433
        for related_key_select in related_key_select_expression_list:
          select_dict[related_key_select] = '%s_%s' % (table,
                                                       related_key_select)
Jérome Perrin committed
434

Vincent Pelletier committed
435
        # Column values
Vincent Pelletier committed
436 437 438
        column_value_dict = new_kw.pop('column_value_dict', {})
        for key, value in column_value_dict.iteritems():
          new_kw['%s.%s' % (table, key)] = value
Vincent Pelletier committed
439
        # Related keys
Vincent Pelletier committed
440 441 442 443 444 445
        # First, the passthrough (acts as default values)
        for key, value in new_kw.pop('related_key_dict_passthrough', {})\
            .iteritems():
          new_kw[key] = value
        # Second, calculated values
        for key, value in new_kw.pop('related_key_dict', {}).iteritems():
Vincent Pelletier committed
446
          new_kw['%s_%s' % (table, key)] = value
Vincent Pelletier committed
447 448 449
        # Simulation states matched with input and output omission
        def getSimulationQuery(simulation_dict, omit_dict):
          simulation_query = self._buildSimulationStateQuery(
Vincent Pelletier committed
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
            simulation_state_dict=simulation_dict,
            table=table,
          )
          query_list = [
            self._getIncreaseQuery(table, column, value)
            for key, column, value in (
              ('input',          'quantity',    False),
              ('output',         'quantity',    True),
              ('asset_increase', 'total_price', False),
              ('asset_decrease', 'total_price', True),
            )
            if omit_dict.get(key)
          ]
          if query_list:
            if simulation_query is not None:
              query_list.append(simulation_query)
            return ComplexQuery(
Vincent Pelletier committed
467
              query_list,
Vincent Pelletier committed
468
              logical_operator='AND',
Vincent Pelletier committed
469 470 471 472 473 474
            )
          return simulation_query
        simulation_query = getSimulationQuery(
          new_kw.pop('simulation_dict', {}),
          new_kw.pop('omit_dict', {}),
        )
Vincent Pelletier committed
475 476 477 478
        reserved_kw = new_kw.pop('reserved_kw', None)
        if reserved_kw is not None:
          reserved_query = getSimulationQuery(
            reserved_kw.pop('simulation_dict', {}),
Vincent Pelletier committed
479 480 481 482 483 484 485 486
            reserved_kw.pop('omit_dict', {}),
          )
          if simulation_query is None:
            simulation_query = reserved_query
          elif reserved_query is not None:
            simulation_query = ComplexQuery(
              simulation_query,
              reserved_query,
Vincent Pelletier committed
487
              logical_operator='OR',
Vincent Pelletier committed
488
            )
Vincent Pelletier committed
489 490
        if simulation_query is not None:
          new_kw['query'] = simulation_query
Jérome Perrin committed
491

Vincent Pelletier committed
492 493
        # Sort on
        if 'sort_on' in new_kw:
Vincent Pelletier committed
494
          table_column_list = ctool.getSQLCatalog().getTableColumnList(table)
Vincent Pelletier committed
495 496 497
          sort_on = new_kw['sort_on']
          new_sort_on = []
          for column_id, sort_direction in sort_on:
Jérome Perrin committed
498
            if column_id in table_column_list:
Vincent Pelletier committed
499 500 501
              column_id = '%s.%s' % (table, column_id)
            new_sort_on.append((column_id, sort_direction))
          new_kw['sort_on'] = tuple(new_sort_on)
Vincent Pelletier committed
502

Jérome Perrin committed
503 504 505 506
        # Remove some internal parameters that does not have any meaning for
        # catalog
        new_kw.pop('ignore_group_by', None)

Vincent Pelletier committed
507 508 509 510 511 512 513 514 515 516 517
        catalog_sql_kw = ctool.buildSQLQuery(**new_kw)
        from_table_dict = dict(sql_kw.pop('from_table_list', []))
        for alias, table in catalog_sql_kw.pop('from_table_list', None) or []:
          assert from_table_dict.get(alias) in (None, table), (
            alias,
            table,
            from_table_dict[alias],
          )
          from_table_dict[alias] = table
        sql_kw.update(catalog_sql_kw)
        sql_kw['from_table_list'] = from_table_dict.items()
Vincent Pelletier committed
518 519
        return sql_kw

Vincent Pelletier committed
520
    def _generateKeywordDict(self,
Jérome Perrin committed
521
        # dates
Alexandre Boeglin committed
522
        from_date=None, to_date=None, at_date=None,
Romain Courteaud committed
523
        omit_mirror_date=1,
Jérome Perrin committed
524
        # instances
Alexandre Boeglin committed
525
        resource=None, node=None, payment=None,
Jérome Perrin committed
526
        section=None, mirror_section=None, item=None,
Jérome Perrin committed
527
        function=None, project=None, funding=None, payment_request=None,
Nicolas Wavrant committed
528
        transformed_resource=None, ledger=None,
Jérome Perrin committed
529 530 531
        # used for tracking
        input=0, output=0,
        # categories
Alexandre Boeglin committed
532
        resource_category=None, node_category=None, payment_category=None,
Alexandre Boeglin committed
533
        section_category=None, mirror_section_category=None,
Jérome Perrin committed
534
        function_category=None, project_category=None, funding_category=None,
Nicolas Wavrant committed
535
        ledger_category=None, payment_request_category=None,
Jérome Perrin committed
536 537 538 539 540 541
        # categories with strict membership
        resource_category_strict_membership=None,
        node_category_strict_membership=None,
        payment_category_strict_membership=None,
        section_category_strict_membership=None,
        mirror_section_category_strict_membership=None,
Jérome Perrin committed
542 543
        function_category_strict_membership=None,
        project_category_strict_membership=None,
Jérome Perrin committed
544
        funding_category_strict_membership=None,
Nicolas Wavrant committed
545
        ledger_category_strict_membership=None,
Jérome Perrin committed
546
        payment_request_category_strict_membership=None,
Jérome Perrin committed
547
        # simulation_state
Guillaume Michon committed
548
        strict_simulation_state=0,
Alexandre Boeglin committed
549
        simulation_state=None, transit_simulation_state = None, omit_transit=0,
Jérome Perrin committed
550
        input_simulation_state=None, output_simulation_state=None,
Sebastien Robin committed
551
        reserved_kw=None,
Jérome Perrin committed
552
        # variations
Sebastien Robin committed
553
        variation_text=None, sub_variation_text=None,
Jérome Perrin committed
554
        variation_category=None,
Nicolas Dumazet committed
555
        transformed_variation_text=None,
Jérome Perrin committed
556
        # uids
Vincent Pelletier committed
557
        resource_uid=None, node_uid=None, section_uid=None, payment_uid=None,
Jérome Perrin committed
558
        mirror_node_uid=None, mirror_section_uid=None, function_uid=None,
Nicolas Wavrant committed
559 560
        project_uid=None, funding_uid=None, ledger_uid=None,
        payment_request_uid=None,
Sebastien Robin committed
561 562 563
        # omit input and output
        omit_input=0,
        omit_output=0,
Jérome Perrin committed
564 565
        omit_asset_increase=0,
        omit_asset_decrease=0,
Sebastien Robin committed
566 567
        # group by
        group_by_node=0,
Jérome Perrin committed
568 569
        group_by_node_category=0,
        group_by_node_category_strict_membership=0,
Sebastien Robin committed
570
        group_by_mirror_node=0,
Jérome Perrin committed
571 572
        group_by_mirror_node_category=0,
        group_by_mirror_node_category_strict_membership=0,
Sebastien Robin committed
573
        group_by_section=0,
Jérome Perrin committed
574 575
        group_by_section_category=0,
        group_by_section_category_strict_membership=0,
Sebastien Robin committed
576
        group_by_mirror_section=0,
Jérome Perrin committed
577 578
        group_by_mirror_section_category=0,
        group_by_mirror_section_category_strict_membership=0,
Sebastien Robin committed
579
        group_by_payment=0,
Jérome Perrin committed
580 581
        group_by_payment_category=0,
        group_by_payment_category_strict_membership=0,
Sebastien Robin committed
582 583 584
        group_by_sub_variation=0,
        group_by_variation=0,
        group_by_movement=0,
Vincent Pelletier committed
585
        group_by_resource=0,
Jérome Perrin committed
586
        group_by_project=0,
Jérome Perrin committed
587 588
        group_by_project_category=0,
        group_by_project_category_strict_membership=0,
Jérome Perrin committed
589 590 591
        group_by_funding=0,
        group_by_funding_category=0,
        group_by_funding_category_strict_membership=0,
Nicolas Wavrant committed
592 593 594
        group_by_ledger=0,
        group_by_ledger_category=0,
        group_by_ledger_category_strict_membership=0,
Jérome Perrin committed
595 596 597
        group_by_payment_request=0,
        group_by_payment_request_category=0,
        group_by_payment_request_category_strict_membership=0,
Jérome Perrin committed
598
        group_by_function=0,
Jérome Perrin committed
599 600
        group_by_function_category=0,
        group_by_function_category_strict_membership=0,
Jérome Perrin committed
601
        group_by_date=0,
Vincent Pelletier committed
602 603
        # sort_on
        sort_on=None,
Aurel committed
604
        group_by=None,
Vincent Pelletier committed
605 606 607
        # selection
        selection_domain=None,
        selection_report=None,
Jérome Perrin committed
608 609
        # keywords for related keys
        **kw):
Alexandre Boeglin committed
610
      """
Romain Courteaud committed
611 612 613 614
      Generates keywords and calls buildSQLQuery

      - omit_mirror_date: normally, date's parameters are only based on date
        column. If 0, it also used the mirror_date column.
Alexandre Boeglin committed
615 616
      """
      new_kw = {}
Vincent Pelletier committed
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
      sql_kw = {
        'from_table_list': [],
        # Set of catalog aliases that must be joined in the ZSQLMethod ('foo'
        # meaning something along the lines of 'foo.uid = stock.foo_uid')
        'selection_domain_catalog_alias_set': [],
        # input and output are used by getTrackingList
        'input': input,
        'output': output,
        # BBB
        'selection_domain': None,
        'selection_report': None,
      }

      if selection_domain is None:
        sql_kw['selection_domain_from_expression'] = None
        sql_kw['selection_domain_where_expression'] = None
      else:
        # Pre-render selection_domain, as it is easier done here than in DTML.
Georgios Dagkakis committed
635 636 637 638 639 640 641 642 643 644
        if isinstance(selection_domain, dict):
          selection_domain_dict = selection_domain
        else:
          selection_domain_dict = selection_domain.asDomainDict()
        if 'ledger' in selection_domain_dict:
          # XXX: what if both 'node' and 'ledger' are present ?
          # Finer configuration may be needed here.
          query_table_alias = 'ledger'
        else:
          query_table_alias = 'node'
Vincent Pelletier committed
645 646 647 648 649 650 651 652 653 654
        selection_domain_sql_dict = self.getPortalObject().portal_catalog.buildSQLQuery(
          selection_domain=selection_domain,
          query_table_alias=query_table_alias,
        )
        sql_kw['selection_domain_from_expression'] = selection_domain_sql_dict['from_expression']
        sql_kw['from_table_list'].extend(selection_domain_sql_dict['from_table_list'])
        sql_kw['selection_domain_where_expression'] = selection_domain_sql_dict['where_expression']
        sql_kw['selection_domain_catalog_alias_set'].append(query_table_alias)
      if selection_report is not None:
        new_kw['selection_report'] = selection_report
Alexandre Boeglin committed
655

Vincent Pelletier committed
656 657 658
      # Add sort_on parameter if defined
      if sort_on is not None:
        new_kw['sort_on'] = sort_on
Guillaume Michon committed
659

Vincent Pelletier committed
660 661
      class DictMixIn(dict):
        def set(dictionary, key, value):
Nicolas Dumazet committed
662
          result = bool(value)
Vincent Pelletier committed
663 664 665 666 667 668 669 670 671
          if result:
            dictionary[key] = value
          return result

        def setUIDList(dictionary, key, value, as_text=0):
          uid_list = self._generatePropertyUidList(value, as_text=as_text)
          return dictionary.set(key, uid_list)

      column_value_dict = DictMixIn()
Romain Courteaud committed
672 673

      if omit_mirror_date:
Jérome Perrin committed
674
        date_dict = {}
Romain Courteaud committed
675
        if from_date :
Jérome Perrin committed
676
          date_dict.setdefault('query', []).append(from_date)
Romain Courteaud committed
677 678
          date_dict['range'] = 'min'
          if to_date :
Jérome Perrin committed
679
            date_dict.setdefault('query', []).append(to_date)
Romain Courteaud committed
680 681
            date_dict['range'] = 'minmax'
          elif at_date :
Jérome Perrin committed
682
            date_dict.setdefault('query', []).append(at_date)
Romain Courteaud committed
683 684
            date_dict['range'] = 'minngt'
        elif to_date :
Jérome Perrin committed
685
          date_dict.setdefault('query', []).append(to_date)
Romain Courteaud committed
686
          date_dict['range'] = 'max'
Jérome Perrin committed
687
        elif at_date :
Jérome Perrin committed
688
          date_dict.setdefault('query', []).append(at_date)
Romain Courteaud committed
689
          date_dict['range'] = 'ngt'
Jérome Perrin committed
690 691
        if date_dict:
          column_value_dict['date'] = date_dict
Romain Courteaud committed
692
      else:
Vincent Pelletier committed
693 694
        column_value_dict['date'] = {'query': [to_date], 'range': 'ngt'}
        column_value_dict['mirror_date'] = {'query': [from_date], 'range': 'nlt'}
Aurel committed
695

Vincent Pelletier committed
696
      column_value_dict.set('resource_uid', resource_uid)
Vincent Pelletier committed
697
      column_value_dict.set('payment_uid', payment_uid)
Jérome Perrin committed
698
      column_value_dict.set('project_uid', project_uid)
Jérome Perrin committed
699
      column_value_dict.set('funding_uid', funding_uid)
Nicolas Wavrant committed
700
      column_value_dict.set('ledger_uid', ledger_uid)
Jérome Perrin committed
701
      column_value_dict.set('payment_request_uid', payment_request_uid)
Jérome Perrin committed
702
      column_value_dict.set('function_uid', function_uid)
Jérome Perrin committed
703
      column_value_dict.set('section_uid', section_uid)
Vincent Pelletier committed
704
      column_value_dict.set('node_uid', node_uid)
Vincent Pelletier committed
705 706
      column_value_dict.set('mirror_node_uid', mirror_node_uid)
      column_value_dict.set('mirror_section_uid', mirror_section_uid)
Vincent Pelletier committed
707 708 709 710
      column_value_dict.setUIDList('resource_uid', resource)
      column_value_dict.setUIDList('aggregate_uid', item)
      column_value_dict.setUIDList('node_uid', node)
      column_value_dict.setUIDList('payment_uid', payment)
Jérome Perrin committed
711
      column_value_dict.setUIDList('project_uid', project)
Jérome Perrin committed
712
      column_value_dict.setUIDList('funding_uid', funding)
Nicolas Wavrant committed
713
      column_value_dict.setUIDList('ledger_uid', ledger)
Jérome Perrin committed
714
      column_value_dict.setUIDList('payment_request_uid', payment_request)
Jérome Perrin committed
715
      column_value_dict.setUIDList('function_uid', function)
Nicolas Dumazet committed
716 717 718

      sql_kw['transformed_uid'] = self._generatePropertyUidList(transformed_resource)

Jérome Perrin committed
719
      column_value_dict.setUIDList('section_uid', section)
Vincent Pelletier committed
720
      column_value_dict.setUIDList('mirror_section_uid', mirror_section)
Jérome Perrin committed
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738

      # Handle variation_category as variation_text
      if variation_category:
        if variation_text:
          raise ValueError(
              "Passing both variation_category and variation_text is not supported")
        warn("variation_category is deprecated, please use variation_text instead",
             DeprecationWarning)
        if isinstance(variation_category, basestring):
          variation_category = (variation_category,)
        # variation text is a \n separated list of variation categories, but without
        # trailing nor leading \n
        variation_text = [
            "{}\n%".format(x) for x in variation_category] + [
            "%\n{}\n%".format(x) for x in variation_category] + [
            "%\n{}".format(x) for x in variation_category] + [
            "{}".format(x) for x in variation_category]

Vincent Pelletier committed
739 740 741 742 743 744 745
      column_value_dict.setUIDList('variation_text', variation_text,
                                   as_text=1)
      column_value_dict.setUIDList('sub_variation_text', sub_variation_text,
                                   as_text=1)
      new_kw['column_value_dict'] = column_value_dict.copy()

      related_key_dict = DictMixIn()
Jérome Perrin committed
746
      # category membership
Vincent Pelletier committed
747 748
      related_key_dict.setUIDList('resource_category_uid', resource_category)
      related_key_dict.setUIDList('node_category_uid', node_category)
Jérome Perrin committed
749
      related_key_dict.setUIDList('project_category_uid', project_category)
Jérome Perrin committed
750
      related_key_dict.setUIDList('funding_category_uid', funding_category)
Nicolas Wavrant committed
751
      related_key_dict.setUIDList('ledger_category_uid', ledger_category)
Jérome Perrin committed
752
      related_key_dict.setUIDList('payment_request_category_uid', payment_request_category)
Jérome Perrin committed
753
      related_key_dict.setUIDList('function_category_uid', function_category)
Vincent Pelletier committed
754
      related_key_dict.setUIDList('payment_category_uid', payment_category)
Jérome Perrin committed
755
      related_key_dict.setUIDList('section_category_uid', section_category)
Vincent Pelletier committed
756 757
      related_key_dict.setUIDList('mirror_section_category_uid',
                                  mirror_section_category)
Jérome Perrin committed
758
      # category strict membership
Vincent Pelletier committed
759 760 761 762
      related_key_dict.setUIDList('resource_category_strict_membership_uid',
                                  resource_category_strict_membership)
      related_key_dict.setUIDList('node_category_strict_membership_uid',
                                  node_category_strict_membership)
Jérome Perrin committed
763 764
      related_key_dict.setUIDList('project_category_strict_membership_uid',
                                  project_category_strict_membership)
Jérome Perrin committed
765 766
      related_key_dict.setUIDList('funding_category_strict_membership_uid',
                                  funding_category_strict_membership)
Nicolas Wavrant committed
767 768
      related_key_dict.setUIDList('ledger_category_strict_membership_uid',
                                  ledger_category_strict_membership)
Jérome Perrin committed
769 770
      related_key_dict.setUIDList('payment_request_category_strict_membership_uid',
                                  payment_request_category_strict_membership)
Jérome Perrin committed
771 772
      related_key_dict.setUIDList('function_category_strict_membership_uid',
                                  function_category_strict_membership)
Vincent Pelletier committed
773 774
      related_key_dict.setUIDList('payment_category_strict_membership_uid',
                                  payment_category_strict_membership)
Jérome Perrin committed
775 776
      related_key_dict.setUIDList('section_category_strict_membership_uid',
                                  section_category_strict_membership)
Vincent Pelletier committed
777 778 779
      related_key_dict.setUIDList(
        'mirror_section_category_strict_membership_uid',
        mirror_section_category_strict_membership)
Vincent Pelletier committed
780

Vincent Pelletier committed
781
      new_kw['related_key_dict'] = related_key_dict.copy()
Vincent Pelletier committed
782
      new_kw['related_key_dict_passthrough'] = kw
Aurel committed
783
      # Check we do not get a known group_by
Vincent Pelletier committed
784
      related_group_by = []
Aurel committed
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
      if group_by:
        if isinstance(group_by, basestring):
          group_by = (group_by,)
        for value in group_by:
          if value == "node_uid":
            group_by_node = 1
          elif value == 'mirror_node_uid':
            group_by_mirror_node = 1
          elif value ==  'section_uid':
            group_by_section = 1
          elif value == 'mirror_section_uid':
            group_by_mirror_section = 1
          elif value == 'payment_uid':
            group_by_payment = 1
          elif value == 'sub_variation_text':
            group_by_sub_variation = 1
          elif value == 'variation_text':
            group_by_variation = 1
          elif value == 'uid':
            group_by_movement = 1
          elif value == 'resource_uid':
            group_by_resource = 1
          elif value == 'project_uid':
            group_by_project = 1
          elif value == 'funding_uid':
            group_by_funding = 1
Nicolas Wavrant committed
811 812
          elif value == 'ledger_uid':
            group_by_ledger = 1
Aurel committed
813 814 815 816 817 818 819 820
          elif value == 'payment_request_uid':
            group_by_payment_request = 1
          elif value == "function_uid":
            group_by_function = 1
          elif value == 'date':
            group_by_date = 1
          else:
            related_group_by.append(value)
Vincent Pelletier committed
821
        if related_group_by:
Kazuhiko Shiozaki committed
822
          new_kw['related_key_dict_passthrough']['group_by_list'] = related_group_by
Vincent Pelletier committed
823

Vincent Pelletier committed
824 825 826 827 828 829 830 831 832 833 834 835 836 837
      new_kw['simulation_dict'] = self._getSimulationStateDict(
        simulation_state=simulation_state,
        omit_transit=omit_transit,
        input_simulation_state=input_simulation_state,
        output_simulation_state=output_simulation_state,
        transit_simulation_state=transit_simulation_state,
        strict_simulation_state=strict_simulation_state,
      )
      new_kw['omit_dict'] = {
        'input': omit_input,
        'output': omit_output,
        'asset_increase': omit_asset_increase,
        'asset_decrease': omit_asset_decrease,
      }
Sebastien Robin committed
838
      if reserved_kw is not None:
Jérome Perrin committed
839
        if not isinstance(reserved_kw, dict):
Vincent Pelletier committed
840
          # Not a dict when taken from URL, so, cast is needed
Romain Courteaud committed
841 842
          # to make pop method available
          reserved_kw = dict(reserved_kw)
Vincent Pelletier committed
843 844 845
        new_kw['reserved_kw'] = {
          'omit_dict': {
            'input': reserved_kw.pop('omit_input', False),
Sebastien Robin committed
846
            'output': reserved_kw.pop('omit_output', False),
Vincent Pelletier committed
847 848 849
          },
          'simulation_dict': self._getSimulationStateDict(**reserved_kw),
        }
Alexandre Boeglin committed
850

Sebastien Robin committed
851
      # build the group by expression
Jérome Perrin committed
852 853 854 855 856
      # if we group by a criterion, we also add this criterion to the select
      # expression, unless it is already selected in Resource_zGetInventoryList
      # the caller can also pass select_dict or select_list. select_expression,
      # which is deprecated in ZSQLCatalog is not supported here.
      select_dict = kw.get('select_dict', {})
Vincent Pelletier committed
857
      select_dict.update(dict.fromkeys(list(kw.pop('select_list', [])) + related_group_by))
Jérome Perrin committed
858 859 860
      new_kw['select_dict'] = select_dict
      related_key_select_expression_list = []

Jérome Perrin committed
861 862
      column_group_by_expression_list = []
      related_key_group_by_expression_list = []
Sebastien Robin committed
863
      if group_by_node:
Jérome Perrin committed
864
        column_group_by_expression_list.append('node_uid')
Sebastien Robin committed
865
      if group_by_mirror_node:
Jérome Perrin committed
866
        column_group_by_expression_list.append('mirror_node_uid')
Sebastien Robin committed
867
      if group_by_section:
Jérome Perrin committed
868
        column_group_by_expression_list.append('section_uid')
Sebastien Robin committed
869
      if group_by_mirror_section:
Jérome Perrin committed
870
        column_group_by_expression_list.append('mirror_section_uid')
Sebastien Robin committed
871
      if group_by_payment:
Jérome Perrin committed
872
        column_group_by_expression_list.append('payment_uid')
Sebastien Robin committed
873
      if group_by_sub_variation:
Jérome Perrin committed
874
        column_group_by_expression_list.append('sub_variation_text')
Sebastien Robin committed
875
      if group_by_variation:
Jérome Perrin committed
876
        column_group_by_expression_list.append('variation_text')
Sebastien Robin committed
877
      if group_by_movement:
Jérome Perrin committed
878
        column_group_by_expression_list.append('uid')
Vincent Pelletier committed
879
      if group_by_resource:
Jérome Perrin committed
880 881 882
        column_group_by_expression_list.append('resource_uid')
      if group_by_project:
        column_group_by_expression_list.append('project_uid')
Jérome Perrin committed
883 884
      if group_by_funding:
        column_group_by_expression_list.append('funding_uid')
Nicolas Wavrant committed
885 886
      if group_by_ledger:
        column_group_by_expression_list.append('ledger_uid')
Jérome Perrin committed
887 888
      if group_by_payment_request:
        column_group_by_expression_list.append('payment_request_uid')
Jérome Perrin committed
889 890
      if group_by_function:
        column_group_by_expression_list.append('function_uid')
Jérome Perrin committed
891
      if group_by_date:
Jérome Perrin committed
892 893 894 895 896 897 898
        column_group_by_expression_list.append('date')

      if column_group_by_expression_list:
        new_kw['column_group_by'] = column_group_by_expression_list

      if group_by_section_category:
        related_key_group_by_expression_list.append('section_category_uid')
Jérome Perrin committed
899
        related_key_select_expression_list.append('section_category_uid')
Jérome Perrin committed
900 901 902
      if group_by_section_category_strict_membership:
        related_key_group_by_expression_list.append(
            'section_category_strict_membership_uid')
Jérome Perrin committed
903 904
        related_key_select_expression_list.append(
            'section_category_strict_membership_uid')
Jérome Perrin committed
905 906
      if group_by_mirror_section_category:
        related_key_group_by_expression_list.append('mirror_section_category_uid')
Jérome Perrin committed
907
        related_key_select_expression_list.append('mirror_section_category_uid')
Jérome Perrin committed
908 909 910
      if group_by_mirror_section_category_strict_membership:
        related_key_group_by_expression_list.append(
            'mirror_section_category_strict_membership_uid')
Jérome Perrin committed
911 912
        related_key_select_expression_list.append(
            'mirror_section_category_strict_membership_uid')
Jérome Perrin committed
913 914
      if group_by_node_category:
        related_key_group_by_expression_list.append('node_category_uid')
Jérome Perrin committed
915
        related_key_select_expression_list.append('node_category_uid')
Jérome Perrin committed
916 917 918
      if group_by_node_category_strict_membership:
        related_key_group_by_expression_list.append(
            'node_category_strict_membership_uid')
Jérome Perrin committed
919 920
        related_key_select_expression_list.append(
            'node_category_strict_membership_uid')
Jérome Perrin committed
921 922 923 924 925
      if group_by_mirror_node_category:
        related_key_group_by_expression_list.append('mirror_node_category_uid')
      if group_by_mirror_node_category_strict_membership:
        related_key_group_by_expression_list.append(
            'mirror_node_category_strict_membership_uid')
Jérome Perrin committed
926 927
        related_key_select_expression_list.append(
            'mirror_node_category_strict_membership_uid')
Jérome Perrin committed
928 929
      if group_by_payment_category:
        related_key_group_by_expression_list.append('payment_category_uid')
Jérome Perrin committed
930
        related_key_select_expression_list.append('payment_category_uid')
Jérome Perrin committed
931 932 933
      if group_by_payment_category_strict_membership:
        related_key_group_by_expression_list.append(
            'payment_category_strict_membership_uid')
Jérome Perrin committed
934 935
        related_key_select_expression_list.append(
            'payment_category_strict_membership_uid')
Jérome Perrin committed
936 937
      if group_by_function_category:
        related_key_group_by_expression_list.append('function_category_uid')
Jérome Perrin committed
938
        related_key_select_expression_list.append('function_category_uid')
Jérome Perrin committed
939 940 941
      if group_by_function_category_strict_membership:
        related_key_group_by_expression_list.append(
            'function_category_strict_membership_uid')
Jérome Perrin committed
942
        related_key_select_expression_list.append(
Jérome Perrin committed
943
            'function_category_strict_membership_uid')
Jérome Perrin committed
944 945 946 947 948 949 950 951
      if group_by_project_category:
        related_key_group_by_expression_list.append('project_category_uid')
        related_key_select_expression_list.append('project_category_uid')
      if group_by_project_category_strict_membership:
        related_key_group_by_expression_list.append(
            'project_category_strict_membership_uid')
        related_key_select_expression_list.append(
            'project_category_strict_membership_uid')
Jérome Perrin committed
952 953 954 955 956 957 958 959
      if group_by_funding_category:
        related_key_group_by_expression_list.append('funding_category_uid')
        related_key_select_expression_list.append('funding_category_uid')
      if group_by_funding_category_strict_membership:
        related_key_group_by_expression_list.append(
            'funding_category_strict_membership_uid')
        related_key_select_expression_list.append(
            'funding_category_strict_membership_uid')
Nicolas Wavrant committed
960 961 962 963 964 965 966 967
      if group_by_ledger_category:
        related_key_group_by_expression_list.append('ledger_category_uid')
        related_key_select_expression_list.append('ledger_category_uid')
      if group_by_ledger_category_strict_membership:
        related_key_group_by_expression_list.append(
            'ledger_category_strict_membership_uid')
        related_key_select_expression_list.append(
            'ledger_category_strict_membership_uid')
Jérome Perrin committed
968 969 970 971 972 973 974 975
      if group_by_payment_category:
        related_key_group_by_expression_list.append('payment_request_category_uid')
        related_key_select_expression_list.append('payment_request_category_uid')
      if group_by_payment_request_category_strict_membership:
        related_key_group_by_expression_list.append(
            'payment_request_category_strict_membership_uid')
        related_key_select_expression_list.append(
            'payment_request_category_strict_membership_uid')
Jérome Perrin committed
976

Jérome Perrin committed
977 978
      if related_key_group_by_expression_list:
        new_kw['related_key_group_by'] = related_key_group_by_expression_list
Jérome Perrin committed
979 980 981
      if related_key_select_expression_list:
        new_kw['related_key_select_expression_list'] =\
                related_key_select_expression_list
Jérome Perrin committed
982 983

      return sql_kw, new_kw
Alexandre Boeglin committed
984

Jean-Paul Smets committed
985
    #######################################################
Aurel committed
986 987
    # Inventory management
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
988
                              'getInventory')
Sebastien Robin committed
989
    def getInventory(self, src__=0, simulation_period='', **kw):
Alexandre Boeglin committed
990
      """
Jérome Perrin committed
991 992
      Returns an inventory of a single or multiple resources on a single or
      multiple nodes as a single float value
Aurel committed
993

Jérome Perrin committed
994
      from_date (>=) - only take rows which date is >= from_date
Alexandre Boeglin committed
995

Jérome Perrin committed
996
      to_date   (<)  - only take rows which date is < to_date
Alexandre Boeglin committed
997 998 999 1000 1001

      at_date   (<=) - only take rows which date is <= at_date

      resource (only in generic API in simulation)

Jérome Perrin committed
1002 1003
      node           -  only take rows in stock table which node_uid is
                        equivalent to node
Alexandre Boeglin committed
1004

Jérome Perrin committed
1005 1006
      payment        -  only take rows in stock table which payment_uid is
                        equivalent to payment
Alexandre Boeglin committed
1007

Jérome Perrin committed
1008 1009
      section        -  only take rows in stock table which section_uid is
                        equivalent to section
Alexandre Boeglin committed
1010

Jean-Paul Smets committed
1011 1012
      mirror_section -  only take rows in stock table which mirror_section_uid is
                        mirror_section
Alexandre Boeglin committed
1013

Jérome Perrin committed
1014 1015
      resource_category  -  only take rows in stock table which
                        resource_uid is member of resource_category
Alexandre Boeglin committed
1016

Jérome Perrin committed
1017 1018
      node_category   - only take rows in stock table which node_uid is
                        member of section_category
Alexandre Boeglin committed
1019

Jean-Paul Smets committed
1020 1021
      payment_category   -  only take rows in stock table which payment_uid
                            is in section_category
Alexandre Boeglin committed
1022

Jérome Perrin committed
1023
      section_category -  only take rows in stock table which section_uid is
Jean-Paul Smets committed
1024 1025
                          member of section_category

Vincent Pelletier committed
1026
      mirror_section_category - only take rows in stock table which
Jean-Paul Smets committed
1027
                                mirror_section_uid is member of
Julien Muchembled committed
1028
                                mirror_section_category
Jean-Paul Smets committed
1029 1030 1031 1032 1033 1034

      node_filter     - only take rows in stock table which node_uid
                        matches node_filter

      payment_filter  - only take rows in stock table which payment_uid
                        matches payment_filter
Alexandre Boeglin committed
1035

Jean-Paul Smets committed
1036 1037 1038
      section_filter  - only take rows in stock table which section_uid
                        matches section_filter

Jérome Perrin committed
1039 1040
      mirror_section_filter - only take rows in stock table which
                              mirror_section_uid matches mirror_section_filter
Alexandre Boeglin committed
1041

Jérome Perrin committed
1042 1043 1044 1045
      variation_text -  only take rows in stock table with specified
                        variation_text.
                        XXX this way of implementing variation selection is far
                        from perfect
Alexandre Boeglin committed
1046

Jérome Perrin committed
1047 1048
      sub_variation_text - only take rows in stock table with specified
                        variation_text
Sebastien Robin committed
1049

Jérome Perrin committed
1050
      variation_category - variation or list of possible variations (it is not
Jérome Perrin committed
1051 1052
                        a cross-search ; SQL query uses OR).
                        Deprecated, use variation_text.
Alexandre Boeglin committed
1053 1054 1055

      simulation_state - only take rows with specified simulation_state

Guillaume Michon committed
1056
      transit_simulation_state - specifies which states are transit states
Alexandre Boeglin committed
1057

Jérome Perrin committed
1058
      omit_transit   -  do not evaluate transit_simulation_state
Alexandre Boeglin committed
1059

Jérome Perrin committed
1060 1061
      input_simulation_state - only take rows with specified simulation_state
                        and quantity > 0
Alexandre Boeglin committed
1062

Jérome Perrin committed
1063 1064
      output_simulation_state - only take rows with specified simulation_state
                        and quantity < 0
Alexandre Boeglin committed
1065

Jérome Perrin committed
1066 1067 1068
      ignore_variation -  do not take into account variation in inventory
                        calculation (useless on getInventory, but useful on
                        getInventoryList)
Alexandre Boeglin committed
1069

Jérome Perrin committed
1070 1071
      standardise    -  provide a standard quantity rather than an SKU (XXX
                        not implemented yet)
Alexandre Boeglin committed
1072

Jérome Perrin committed
1073 1074
      omit_simulation - doesn't take into account simulation movements

Jérome Perrin committed
1075 1076 1077 1078
      only_accountable - Only take into account accountable movements. By
                        default, only movements for which isAccountable() is
                        true will be taken into account.

Jérome Perrin committed
1079 1080 1081 1082 1083
      omit_input     -  doesn't take into account movement with quantity > 0

      omit_output    -  doesn't take into account movement with quantity < 0

      omit_asset_increase - doesn't take into account movement with asset_price > 0
Alexandre Boeglin committed
1084

Jérome Perrin committed
1085
      omit_asset_decrease - doesn't take into account movement with asset_price < 0
Alexandre Boeglin committed
1086 1087 1088

      selection_domain, selection_report - see ListBox

Jérome Perrin committed
1089 1090
      group_by_variation - (useless on getInventory, but useful on
                        getInventoryList)
Sebastien Robin committed
1091

Jérome Perrin committed
1092 1093
      group_by_node  -  (useless on getInventory, but useful on
                        getInventoryList)
Sebastien Robin committed
1094

Jérome Perrin committed
1095 1096
      group_by_mirror_node - (useless on getInventory, but useful on
                        getInventoryList)
Sebastien Robin committed
1097

Jérome Perrin committed
1098 1099
      group_by_sub_variation - (useless on getInventory, but useful on
                        getInventoryList)
Sebastien Robin committed
1100

Sebastien Robin committed
1101 1102 1103
      group_by_movement - (useless on getInventory, but useful on
                        getInventoryList)

Jérome Perrin committed
1104 1105
      precision - the precision used to round quantities and prices.

Nicolas Dumazet committed
1106 1107 1108 1109
      metric_type   - convert the results to a specific metric_type

      quantity_unit - display results using this specific quantity unit

Nicolas Dumazet committed
1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
      transformed_resource - one, or several resources. list resources that can
                             be produced using those resources as input in a
                             transformation.
                             relative_resource_url for each returned line will
                             point to the transformed resource, while the stock
                             will be the stock of the produced resource,
                             expressed in number of transformed resources.
      transformed_variation_text - to be used with transformed_resource, to
                                   to refine the transformation selection only
                                   to those using variated resources as input.

Jérome Perrin committed
1121 1122
      **kw           -  if we want extended selection with more keywords (but
                        bad performance) check what we can do with
Jean-Paul Smets committed
1123
                        buildSQLQuery
Aurel committed
1124

Jérome Perrin committed
1125 1126
      NOTE: we may want to define a parameter so that we can select the kind of
      inventory statistics we want to display (ex. sum, average, cost, etc.)
Alexandre Boeglin committed
1127
      """
Jean-Paul Smets committed
1128 1129 1130 1131 1132 1133 1134 1135
      # JPS: this is a hint for implementation of xxx_filter arguments
      # node_uid_count = portal_catalog.countResults(**node_filter)
      # if node_uid_count not too big:
      #   node_uid_list = cache(portal_catalog(**node_filter))
      #   pass this list to ZSQL method
      # else:
      #   build a table in MySQL
      #   and join that table with the stock table
Sebastien Robin committed
1136
      method = getattr(self,'get%sInventoryList' % simulation_period)
Jérome Perrin committed
1137 1138
      kw['ignore_group_by'] = 1
      result = method(inventory_list=0, src__=src__, **kw)
Jérome Perrin committed
1139
      if src__:
Alexandre Boeglin committed
1140 1141
        return result

Alexandre Boeglin committed
1142 1143
      total_result = 0.0
      if len(result) > 0:
Sebastien Robin committed
1144 1145
        if len(result) != 1:
          raise ValueError, 'Sorry we must have only one'
Nicolas Dumazet committed
1146 1147 1148 1149 1150 1151 1152 1153
        result = result[0]

        if hasattr(result, "converted_quantity"):
          total_result = result.converted_quantity
        else:
          inventory = result.total_quantity
          if inventory is not None:
            total_result = inventory
Alexandre Boeglin committed
1154 1155

      return total_result
Jean-Paul Smets committed
1156

Aurel committed
1157
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
1158
                              'getCurrentInventory')
Sebastien Robin committed
1159
    def getCurrentInventory(self, **kw):
Alexandre Boeglin committed
1160 1161 1162
      """
      Returns current inventory
      """
Sebastien Robin committed
1163
      return self.getInventory(simulation_period='Current', **kw)
Alexandre Boeglin committed
1164

Aurel committed
1165
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
1166
                              'getAvailableInventory')
Alexandre Boeglin committed
1167 1168 1169
    def getAvailableInventory(self, **kw):
      """
      Returns available inventory
Sebastien Robin committed
1170
      (current inventory - reserved_inventory)
Alexandre Boeglin committed
1171
      """
Sebastien Robin committed
1172
      return self.getInventory(simulation_period='Available', **kw)
Alexandre Boeglin committed
1173

Aurel committed
1174
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
1175
                              'getFutureInventory')
Alexandre Boeglin committed
1176 1177 1178 1179
    def getFutureInventory(self, **kw):
      """
      Returns future inventory
      """
Sebastien Robin committed
1180
      return self.getInventory(simulation_period='Future', **kw)
Vincent Pelletier committed
1181 1182

    def _getDefaultGroupByParameters(self, ignore_group_by=0,
Vincent Pelletier committed
1183 1184
        group_by_node=0, group_by_mirror_node=0,
        group_by_section=0, group_by_mirror_section=0,
Jérome Perrin committed
1185
        group_by_payment=0, group_by_project=0, group_by_funding=0,
Nicolas Wavrant committed
1186
        group_by_ledger=0, group_by_function=0,
Vincent Pelletier committed
1187
        group_by_variation=0, group_by_sub_variation=0,
Jérome Perrin committed
1188
        group_by_movement=0, group_by_date=0,
Jérome Perrin committed
1189 1190
        group_by_section_category=0,
        group_by_section_category_strict_membership=0,
Vincent Pelletier committed
1191
        group_by_resource=None,
Jérome Perrin committed
1192
        group_by=None,
Vincent Pelletier committed
1193
        **ignored):
Sebastien Robin committed
1194 1195
      """
      Set defaults group_by parameters
Vincent Pelletier committed
1196 1197 1198 1199 1200

      If ignore_group_by is true, this function returns an empty dict.

      If any group-by is provided, automatically group by resource aswell
      unless group_by_resource is explicitely set to false.
Jérome Perrin committed
1201
      If no group by is provided, use the default group by: movement, node and
Jérome Perrin committed
1202
      resource.
Sebastien Robin committed
1203
      """
Vincent Pelletier committed
1204
      new_group_by_dict = {}
Jérome Perrin committed
1205
      if not ignore_group_by and group_by is None:
Vincent Pelletier committed
1206
        if group_by_node or group_by_mirror_node or group_by_section or \
Nicolas Wavrant committed
1207 1208
           group_by_project or group_by_funding or group_by_ledger or \
           group_by_function or group_by_mirror_section or group_by_payment or \
Jérome Perrin committed
1209
           group_by_sub_variation or group_by_variation or \
Jérome Perrin committed
1210 1211
           group_by_movement or group_by_date or group_by_section_category or\
           group_by_section_category_strict_membership:
Vincent Pelletier committed
1212 1213
          if group_by_resource is None:
            group_by_resource = 1
Vincent Pelletier committed
1214
          new_group_by_dict['group_by_resource'] = group_by_resource
Vincent Pelletier committed
1215
        elif group_by_resource is None:
Vincent Pelletier committed
1216 1217 1218 1219
          new_group_by_dict['group_by_movement'] = 1
          new_group_by_dict['group_by_node'] = 1
          new_group_by_dict['group_by_resource'] = 1
      return new_group_by_dict
Alexandre Boeglin committed
1220

Aurel committed
1221
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
1222
                              'getInventoryList')
Vincent Pelletier committed
1223 1224
    def getInventoryList(self, src__=0, optimisation__=True,
                         ignore_variation=0, standardise=0,
Vincent Pelletier committed
1225
                         omit_simulation=0,
Jérome Perrin committed
1226
                         only_accountable=True,
Sebastien Robin committed
1227
                         default_stock_table='stock',
Vincent Pelletier committed
1228
                         statistic=0, inventory_list=1,
Julien Muchembled committed
1229
                         precision=None, connection_id=None,
Nicolas Dumazet committed
1230
                         **kw):
Alexandre Boeglin committed
1231
      """
Aurel committed
1232 1233 1234
        Returns a list of inventories for a single or multiple
        resources on a single or multiple nodes, grouped by resource,
        node, section, etc. Every line defines an inventory value for
Romain Courteaud committed
1235
        a given group of resource, node, section.
Aurel committed
1236 1237
        NOTE: we may want to define a parameter so that we can select
        the kind of inventory statistics we want to display (ex. sum,
Romain Courteaud committed
1238
        average, cost, etc.)
Vincent Pelletier committed
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248

        Optimisation queries.
        Optimisation of a stock lookup is done to avoid a table scan
        of all lines corresponding to a given node, section or payment,
        because they grow with time and query time should not.
        First query: Fetch fitting full inventory dates.
          For each node, section or payment, find the first anterior full
          inventory.
        Second query: Fetch full inventory amounts.
          Fetch values of inventory identified in the first query.
1249
        Third query: Classic stock table read.
Vincent Pelletier committed
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262
          Fetch all rows in stock table which are posterior to the inventory.
        Final result
          Add results of the second and third queries, and return it.

        Missing optimisations:
         - In a getInventory case where everything is specified for the
           resource, it's not required for the inventory to be full, it
           just need to be done for the right resource.
           If the resource isn't completely defined, we require inventory
           to be full, which is implemented.
         - Querying multiple nodes/categories/payments in one call prevents
           from using optimisation, it should be equivalent to multiple calls
           on individual nodes/categories/payments.
Vincent Pelletier committed
1263
         -
Alexandre Boeglin committed
1264
      """
Nicolas Dumazet committed
1265 1266 1267 1268
      getCategory = self.getPortalObject().portal_categories.getCategoryUid

      result_column_id_dict = {}

Nicolas Dumazet committed
1269
      metric_type = kw.pop('metric_type', None)
Nicolas Dumazet committed
1270 1271
      quantity_unit = kw.pop('quantity_unit', None)
      quantity_unit_uid = None
Nicolas Dumazet committed
1272

Nicolas Dumazet committed
1273 1274 1275 1276 1277 1278
      if quantity_unit is not None:

        if isinstance(quantity_unit, str):
          quantity_unit_uid = getCategory(quantity_unit, 'quantity_unit')
          if quantity_unit_uid is not None:
            result_column_id_dict['converted_quantity'] = None
Nicolas Dumazet committed
1279 1280 1281
            if metric_type is None:
              # use the default metric type
              metric_type = quantity_unit.split("/", 1)[0]
Kazuhiko Shiozaki committed
1282
        elif isinstance(quantity_unit, (int, float)):
Nicolas Dumazet committed
1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
          # quantity_unit used to be a numerical parameter..
          raise ValueError('Numeric values for quantity_unit are not supported')


      convert_quantity_result = False
      if metric_type is not None:
        metric_type_uid = getCategory(metric_type, 'metric_type')

        if metric_type_uid is not None:
          convert_quantity_result = True
          kw['metric_type_uid'] = Query(
                                    metric_type_uid=metric_type_uid,
                                    table_alias_list=(("measure", "measure"),))

Vincent Pelletier committed
1297 1298
      if src__:
        sql_source_list = []
Sebastien Robin committed
1299
      # If no group at all, give a default sort group by
Vincent Pelletier committed
1300
      kw.update(self._getDefaultGroupByParameters(**kw))
Aurel committed
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316
      base_inventory_kw = {
        'stock_table_id': default_stock_table,
        'src__': src__,
        'ignore_variation': ignore_variation,
        'standardise': standardise,
        'omit_simulation': omit_simulation,
        'only_accountable': only_accountable,
        'precision': precision,
        'inventory_list': inventory_list,
        'connection_id': connection_id,
        'statistic': statistic,
        'convert_quantity_result': convert_quantity_result,
        'quantity_unit_uid': quantity_unit_uid,
      }
      # Get cached data
      if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \
Aurel committed
1317
              optimisation__ and (not kw.get('from_date')) and \
Aurel committed
1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
              'transformed_resource' not in kw:
        # Here is the different kind of date
        # from_date : >=
        # to_date   : <
        # at_date   : <=
        # As we just have from_date, it means that we must use
        # the to_date for the cache in order to avoid double computation
        # of the same line
        at_date = kw.pop("at_date", None)
        if at_date is None:
Tatuya Kamada committed
1328
          to_date = kw.pop("to_date", None)
Aurel committed
1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347
        else:
          # add one second so that we can use to_date
          to_date = at_date + MYSQL_MIN_DATETIME_RESOLUTION
        try:
          cached_result, cached_date = self._getCachedInventoryList(
              to_date=to_date,
              sql_kw=kw,
              **base_inventory_kw)
        except StockOptimisationError:
          cached_result = []
          kw['to_date'] = to_date
        else:
          if src__:
            sql_source_list.extend(cached_result)
          # Now must generate query for date diff
          kw['to_date'] = to_date
          kw['from_date'] = cached_date
      else:
        cached_result = []
Vincent Pelletier committed
1348
      sql_kw, new_kw = self._generateKeywordDict(**kw)
Aurel committed
1349 1350 1351 1352 1353 1354 1355 1356 1357
      # Copy kw content as _generateSQLKeywordDictFromKeywordDict
      # remove some values from it
      try:
        new_kw_copy = deepcopy(new_kw)
      except TypeError:
        # new_kw contains wrong parameters
        # as optimisation has already been disable we
        # do not care about the deepcopy
        new_kw_copy = new_kw
Vincent Pelletier committed
1358
      stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
Aurel committed
1359 1360 1361 1362
          table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy)
      stock_sql_kw.update(base_inventory_kw)
      delta_result = self.Resource_zGetInventoryList(
          **stock_sql_kw)
Vincent Pelletier committed
1363
      if src__:
Aurel committed
1364
        sql_source_list.append(delta_result)
Vincent Pelletier committed
1365
        result = ';\n-- NEXT QUERY\n'.join(sql_source_list)
Aurel committed
1366 1367 1368 1369 1370
      else:
        if cached_result:
            result = self._addBrainResults(delta_result, cached_result, new_kw)
        else:
            result = delta_result
Vincent Pelletier committed
1371
      return result
Romain Courteaud committed
1372

Kazuhiko Shiozaki committed
1373 1374
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryCacheLag')
Aurel committed
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
    def getInventoryCacheLag(self):
      """
      Returns a duration, in days, for stock cache management.
      If data in stock cache is older than lag compared to query's date
      (at_date or to_date), then it becomes a "soft miss": use found value,
      but add a new entry to cache at query's date minus half the lag.
      So this value should be:
      - Small enough that few enough rows need to be table-scanned for
        average queries (probably queries against current date).
      - Large enough that few enough documents get modified past that date,
        otherwise cache entries would be removed from cache all the time.
      """
      return self.SimulationTool_getInventoryCacheLag()

    def _getCachedInventoryList(self, to_date, sql_kw, stock_table_id, src__=False, **kw):
      """
      Try to get a cached inventory list result
      If not existing, fill the cache
      """
      Resource_zGetInventoryList = self.Resource_zGetInventoryList
      # Generate the SQL source without date parameter
      # This will be the cache key
      try:
          no_date_kw = deepcopy(sql_kw)
      except TypeError:
          LOG("SimulationTool._getCachedInventoryList", WARNING,
              "Failed copying sql_kw, disabling stock cache",
Vincent Pelletier committed
1402
              error=True)
Aurel committed
1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417
          raise StockOptimisationError
      no_date_sql_kw, no_date_new_kw = self._generateKeywordDict(**no_date_kw)
      no_date_stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
        table=stock_table_id, sql_kw=no_date_sql_kw,
        new_kw=no_date_new_kw)
      kw.update(no_date_stock_sql_kw)
      if src__:
        sql_source_list = []
      # Generate the cache key (md5 of query source)
      sql_text_hash = md5(Resource_zGetInventoryList(
        stock_table_id=stock_table_id,
        src__=1,
        **kw)).digest()
      # Try to get result from cache
      Resource_zGetInventoryCacheResult = self.Resource_zGetInventoryCacheResult
Tatuya Kamada committed
1418 1419 1420
      inventory_cache_kw = {'query': sql_text_hash}
      if to_date is not None:
        inventory_cache_kw['date'] = to_date
Aurel committed
1421 1422
      try:
          cached_sql_result = Resource_zGetInventoryCacheResult(**inventory_cache_kw)
Sebastien Robin committed
1423 1424 1425
      except ProgrammingError, (code, _):
          if code != NO_SUCH_TABLE:
            raise
Aurel committed
1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450
          # First use of the optimisation, we need to create the table
          LOG("SimulationTool._getCachedInventoryList", INFO,
              "Creating inventory cache stock")
          if src__:
              sql_source_list.append(self.SimulationTool_zCreateInventoryCache(src__=1))
          else:
              self.SimulationTool_zCreateInventoryCache()
          cached_sql_result = None

      if src__:
        sql_source_list.append(Resource_zGetInventoryCacheResult(src__=1, **inventory_cache_kw))
      if cached_sql_result:
        brain_result = loads(cached_sql_result[0].result)
        # Rebuild the brains
        cached_result = Results(
          (brain_result['items'], brain_result['data']),
          brains=getBrain(
            Resource_zGetInventoryList.class_file_,
            Resource_zGetInventoryList.class_name_,
          ),
          parent=self,
        )
      else:
        cached_result = []
      cache_lag = self.getInventoryCacheLag()
Tatuya Kamada committed
1451
      if cached_sql_result and (to_date is None or (to_date - DateTime(cached_sql_result[0].date) < cache_lag)):