Blame view

product/ERP5/Tool/SimulationTool.py 134 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 67
from cPickle import loads, dumps
from copy import deepcopy
from sys import exc_info

MYSQL_MIN_DATETIME_RESOLUTION = 1/86400.

class StockOptimisationError(Exception):
    pass
Nicolas Dumazet committed
68

Romain Courteaud committed
69
class SimulationTool(BaseTool):
Jean-Paul Smets committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
    """
    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
89
    portal_type = 'Simulation Tool'
Jean-Paul Smets committed
90 91 92 93 94 95 96 97 98 99 100 101
    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
102 103 104 105 106 107 108
      # 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
109

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

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

Jérome Perrin committed
118 119 120 121 122 123 124 125 126 127 128 129
      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
130

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

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

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

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

Łukasz Nowak committed
157 158 159
    def _solveMovementOrDelivery(self, document, delivery_solver_name,
                                 target_solver_name, movement=0, delivery=0,
                                 additional_parameters=None,**kw):
Jean-Paul Smets committed
160
      """
Łukasz Nowak committed
161
        Solves a document by calling first DeliverySolver, then TargetSolver
Jean-Paul Smets committed
162
      """
Łukasz Nowak committed
163 164 165 166 167 168 169 170
      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
171 172 173 174 175 176
        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
177 178
          solver = solver_class(additional_parameters=additional_parameters,
              **kw)
Romain Courteaud committed
179

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

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

Jérome Perrin committed
190
    def _generatePropertyUidList(self, prop, as_text=0):
Alexandre Boeglin committed
191 192 193 194 195 196 197
      """
      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
198
      if prop is None :
Jérome Perrin committed
199 200
        return []
      category_tool = getToolByName(self, 'portal_categories')
Alexandre Boeglin committed
201
      property_uid_list = []
Jérome Perrin committed
202
      if isinstance(prop, str):
Jérome Perrin committed
203
        if not as_text:
Jérome Perrin committed
204
          prop_value = category_tool.getCategoryValue(prop)
Jérome Perrin committed
205
          if prop_value is None:
Jérome Perrin committed
206
            raise ValueError, 'Category %s does not exists' % prop
Jérome Perrin committed
207
          property_uid_list.append(prop_value.getUid())
Alexandre Boeglin committed
208
        else:
Jérome Perrin committed
209 210 211
          property_uid_list.append(prop)
      elif isinstance(prop, (list, tuple)):
        for property_item in prop :
Jérome Perrin committed
212
          if not as_text:
Jérome Perrin committed
213 214 215 216
            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
217 218
          else:
            property_uid_list.append(property_item)
Jérome Perrin committed
219
      elif isinstance(prop, dict):
Alexandre Boeglin committed
220
        tmp_uid_list = []
Jérome Perrin committed
221 222 223
        if isinstance(prop['query'], str):
          prop['query'] = [prop['query']]
        for property_item in prop['query'] :
Jérome Perrin committed
224
          if not as_text:
Jérome Perrin committed
225 226 227 228
            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
229 230
          else:
            tmp_uid_list.append(property_item)
Jérome Perrin committed
231
        if tmp_uid_list:
Alexandre Boeglin committed
232
          property_uid_list = {}
Jérome Perrin committed
233
          property_uid_list['operator'] = prop['operator']
Alexandre Boeglin committed
234 235 236
          property_uid_list['query'] = tmp_uid_list
      return property_uid_list

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

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

    def _getSimulationStateDict(self, simulation_state=None, omit_transit=0,
Sebastien Robin committed
266 267 268 269 270 271 272 273 274 275 276 277
                                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
278
      simulation_dict = {}
Sebastien Robin committed
279 280 281
      if strict_simulation_state:
        if isinstance(simulation_state, string_or_list)\
                and simulation_state:
Vincent Pelletier committed
282
           simulation_query = SimpleQuery(
Romain Courteaud committed
283
                   **{'stock.simulation_state': simulation_state})
Sebastien Robin committed
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 338
      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
339
              simulation_dict['simulation_state'] = input_simulation_state
Sebastien Robin committed
340
            else:
Vincent Pelletier committed
341 342
              simulation_dict['input_simulation_state'] = input_simulation_state
              simulation_dict['output_simulation_state'] = output_simulation_state
Sebastien Robin committed
343
          else:
Vincent Pelletier committed
344
            simulation_dict['input_simulation_state'] = input_simulation_state
Sebastien Robin committed
345
        elif output_simulation_state is not None:
Vincent Pelletier committed
346 347
          simulation_dict['simulation_state'] = output_simulation_state
      return simulation_dict
Sebastien Robin committed
348

Vincent Pelletier committed
349
    def _getIncreaseQuery(self, table, column, increase, sql_catalog_id=None):
Sebastien Robin committed
350
      """
Vincent Pelletier committed
351 352 353 354 355 356 357 358 359 360 361 362 363 364
      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
365
      """
Vincent Pelletier committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
      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
382
          logical_operator='AND',
Vincent Pelletier committed
383 384 385 386
        ),
        ComplexQuery(
          SimpleQuery(comparison_operator='>=', **{table + '.' + column: 0}),
          SimpleQuery(**{table + '.is_cancellation': not increase}),
Vincent Pelletier committed
387
          logical_operator='AND',
Vincent Pelletier committed
388
        ),
Vincent Pelletier committed
389
        logical_operator='OR',
Vincent Pelletier committed
390
      )
Vincent Pelletier committed
391

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

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

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

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

        # group by from related keys columns (eg. group_by_node_category=True)
Jérome Perrin committed
413 414 415
        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
416

Jérome Perrin committed
417 418
        # group by involving a related key (eg. group_by=['product_line_uid'])
        related_key_dict_passthrough_group_by = new_kw.get(
Kazuhiko Shiozaki committed
419
                'related_key_dict_passthrough', {}).pop('group_by_list', [])
Jérome Perrin committed
420 421 422
        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
423 424
        group_by.extend(related_key_dict_passthrough_group_by)

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

Jérome Perrin committed
428
        # select expression
Julien Muchembled committed
429
        select_dict = new_kw.setdefault('select_dict', {})
Jérome Perrin committed
430 431
        related_key_select_expression_list = new_kw.pop(
                'related_key_select_expression_list', [])
Jérome Perrin committed
432 433 434
        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
435

Vincent Pelletier committed
436
        # Column values
Vincent Pelletier committed
437 438 439
        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
440
        # Related keys
Vincent Pelletier committed
441 442 443 444 445 446
        # 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
447
          new_kw['%s_%s' % (table, key)] = value
Vincent Pelletier committed
448 449 450
        # Simulation states matched with input and output omission
        def getSimulationQuery(simulation_dict, omit_dict):
          simulation_query = self._buildSimulationStateQuery(
Vincent Pelletier committed
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
            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
468
              query_list,
Vincent Pelletier committed
469
              logical_operator='AND',
Vincent Pelletier committed
470 471 472 473 474 475
            )
          return simulation_query
        simulation_query = getSimulationQuery(
          new_kw.pop('simulation_dict', {}),
          new_kw.pop('omit_dict', {}),
        )
Vincent Pelletier committed
476 477 478 479
        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
480 481 482 483 484 485 486 487
            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
488
              logical_operator='OR',
Vincent Pelletier committed
489
            )
Vincent Pelletier committed
490 491
        if simulation_query is not None:
          new_kw['query'] = simulation_query
Jérome Perrin committed
492

Vincent Pelletier committed
493 494
        # Sort on
        if 'sort_on' in new_kw:
Vincent Pelletier committed
495
          table_column_list = ctool.getSQLCatalog().getTableColumnList(table)
Vincent Pelletier committed
496 497 498
          sort_on = new_kw['sort_on']
          new_sort_on = []
          for column_id, sort_direction in sort_on:
Jérome Perrin committed
499
            if column_id in table_column_list:
Vincent Pelletier committed
500 501 502
              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
503

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

Vincent Pelletier committed
508 509 510 511 512 513 514 515 516 517 518
        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
519 520
        return sql_kw

Vincent Pelletier committed
521
    def _generateKeywordDict(self,
Jérome Perrin committed
522
        # dates
Alexandre Boeglin committed
523
        from_date=None, to_date=None, at_date=None,
Romain Courteaud committed
524
        omit_mirror_date=1,
Jérome Perrin committed
525
        # instances
Alexandre Boeglin committed
526
        resource=None, node=None, payment=None,
Jérome Perrin committed
527
        section=None, mirror_section=None, item=None,
Jérome Perrin committed
528
        function=None, project=None, funding=None, payment_request=None,
Nicolas Wavrant committed
529
        transformed_resource=None, ledger=None,
Jérome Perrin committed
530 531 532
        # used for tracking
        input=0, output=0,
        # categories
Alexandre Boeglin committed
533
        resource_category=None, node_category=None, payment_category=None,
Alexandre Boeglin committed
534
        section_category=None, mirror_section_category=None,
Jérome Perrin committed
535
        function_category=None, project_category=None, funding_category=None,
Nicolas Wavrant committed
536
        ledger_category=None, payment_request_category=None,
Jérome Perrin committed
537 538 539 540 541 542
        # 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
543 544
        function_category_strict_membership=None,
        project_category_strict_membership=None,
Jérome Perrin committed
545
        funding_category_strict_membership=None,
Nicolas Wavrant committed
546
        ledger_category_strict_membership=None,
Jérome Perrin committed
547
        payment_request_category_strict_membership=None,
Jérome Perrin committed
548
        # simulation_state
Guillaume Michon committed
549
        strict_simulation_state=0,
Alexandre Boeglin committed
550
        simulation_state=None, transit_simulation_state = None, omit_transit=0,
Jérome Perrin committed
551
        input_simulation_state=None, output_simulation_state=None,
Sebastien Robin committed
552
        reserved_kw=None,
Jérome Perrin committed
553
        # variations
Sebastien Robin committed
554
        variation_text=None, sub_variation_text=None,
Jérome Perrin committed
555
        variation_category=None,
Nicolas Dumazet committed
556
        transformed_variation_text=None,
Jérome Perrin committed
557
        # uids
Vincent Pelletier committed
558
        resource_uid=None, node_uid=None, section_uid=None, payment_uid=None,
Jérome Perrin committed
559
        mirror_node_uid=None, mirror_section_uid=None, function_uid=None,
Nicolas Wavrant committed
560 561
        project_uid=None, funding_uid=None, ledger_uid=None,
        payment_request_uid=None,
Sebastien Robin committed
562 563 564
        # omit input and output
        omit_input=0,
        omit_output=0,
Jérome Perrin committed
565 566
        omit_asset_increase=0,
        omit_asset_decrease=0,
Sebastien Robin committed
567 568
        # group by
        group_by_node=0,
Jérome Perrin committed
569 570
        group_by_node_category=0,
        group_by_node_category_strict_membership=0,
Sebastien Robin committed
571
        group_by_mirror_node=0,
Jérome Perrin committed
572 573
        group_by_mirror_node_category=0,
        group_by_mirror_node_category_strict_membership=0,
Sebastien Robin committed
574
        group_by_section=0,
Jérome Perrin committed
575 576
        group_by_section_category=0,
        group_by_section_category_strict_membership=0,
Sebastien Robin committed
577
        group_by_mirror_section=0,
Jérome Perrin committed
578 579
        group_by_mirror_section_category=0,
        group_by_mirror_section_category_strict_membership=0,
Sebastien Robin committed
580
        group_by_payment=0,
Jérome Perrin committed
581 582
        group_by_payment_category=0,
        group_by_payment_category_strict_membership=0,
Sebastien Robin committed
583 584 585
        group_by_sub_variation=0,
        group_by_variation=0,
        group_by_movement=0,
Vincent Pelletier committed
586
        group_by_resource=0,
Jérome Perrin committed
587
        group_by_project=0,
Jérome Perrin committed
588 589
        group_by_project_category=0,
        group_by_project_category_strict_membership=0,
Jérome Perrin committed
590 591 592
        group_by_funding=0,
        group_by_funding_category=0,
        group_by_funding_category_strict_membership=0,
Nicolas Wavrant committed
593 594 595
        group_by_ledger=0,
        group_by_ledger_category=0,
        group_by_ledger_category_strict_membership=0,
Jérome Perrin committed
596 597 598
        group_by_payment_request=0,
        group_by_payment_request_category=0,
        group_by_payment_request_category_strict_membership=0,
Jérome Perrin committed
599
        group_by_function=0,
Jérome Perrin committed
600 601
        group_by_function_category=0,
        group_by_function_category_strict_membership=0,
Jérome Perrin committed
602
        group_by_date=0,
Vincent Pelletier committed
603 604
        # sort_on
        sort_on=None,
Aurel committed
605
        group_by=None,
Vincent Pelletier committed
606 607 608
        # selection
        selection_domain=None,
        selection_report=None,
Jérome Perrin committed
609 610
        # keywords for related keys
        **kw):
Alexandre Boeglin committed
611
      """
Romain Courteaud committed
612 613 614 615
      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
616 617
      """
      new_kw = {}
Vincent Pelletier committed
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
      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
636 637 638 639 640 641 642 643 644 645
        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
646 647 648 649 650 651 652 653 654 655
        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
656

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

Vincent Pelletier committed
661 662
      class DictMixIn(dict):
        def set(dictionary, key, value):
Nicolas Dumazet committed
663
          result = bool(value)
Vincent Pelletier committed
664 665 666 667 668 669 670 671 672
          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
673 674

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

Vincent Pelletier committed
697
      column_value_dict.set('resource_uid', resource_uid)
Vincent Pelletier committed
698
      column_value_dict.set('payment_uid', payment_uid)
Jérome Perrin committed
699
      column_value_dict.set('project_uid', project_uid)
Jérome Perrin committed
700
      column_value_dict.set('funding_uid', funding_uid)
Nicolas Wavrant committed
701
      column_value_dict.set('ledger_uid', ledger_uid)
Jérome Perrin committed
702
      column_value_dict.set('payment_request_uid', payment_request_uid)
Jérome Perrin committed
703
      column_value_dict.set('function_uid', function_uid)
Jérome Perrin committed
704
      column_value_dict.set('section_uid', section_uid)
Vincent Pelletier committed
705
      column_value_dict.set('node_uid', node_uid)
Vincent Pelletier committed
706 707
      column_value_dict.set('mirror_node_uid', mirror_node_uid)
      column_value_dict.set('mirror_section_uid', mirror_section_uid)
Vincent Pelletier committed
708 709 710 711
      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
712
      column_value_dict.setUIDList('project_uid', project)
Jérome Perrin committed
713
      column_value_dict.setUIDList('funding_uid', funding)
Nicolas Wavrant committed
714
      column_value_dict.setUIDList('ledger_uid', ledger)
Jérome Perrin committed
715
      column_value_dict.setUIDList('payment_request_uid', payment_request)
Jérome Perrin committed
716
      column_value_dict.setUIDList('function_uid', function)
Nicolas Dumazet committed
717 718 719

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

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

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

Vincent Pelletier committed
782
      new_kw['related_key_dict'] = related_key_dict.copy()
Vincent Pelletier committed
783
      new_kw['related_key_dict_passthrough'] = kw
Aurel committed
784
      # Check we do not get a known group_by
Vincent Pelletier committed
785
      related_group_by = []
Aurel committed
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 811
      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
812 813
          elif value == 'ledger_uid':
            group_by_ledger = 1
Aurel committed
814 815 816 817 818 819 820 821
          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
822
        if related_group_by:
Kazuhiko Shiozaki committed
823
          new_kw['related_key_dict_passthrough']['group_by_list'] = related_group_by
Vincent Pelletier committed
824

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

Sebastien Robin committed
852
      # build the group by expression
Jérome Perrin committed
853 854 855 856 857
      # 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
858
      select_dict.update(dict.fromkeys(list(kw.pop('select_list', [])) + related_group_by))
Jérome Perrin committed
859 860 861
      new_kw['select_dict'] = select_dict
      related_key_select_expression_list = []

Jérome Perrin committed
862 863
      column_group_by_expression_list = []
      related_key_group_by_expression_list = []
Sebastien Robin committed
864
      if group_by_node:
Jérome Perrin committed
865
        column_group_by_expression_list.append('node_uid')
Sebastien Robin committed
866
      if group_by_mirror_node:
Jérome Perrin committed
867
        column_group_by_expression_list.append('mirror_node_uid')
Sebastien Robin committed
868
      if group_by_section:
Jérome Perrin committed
869
        column_group_by_expression_list.append('section_uid')
Sebastien Robin committed
870
      if group_by_mirror_section:
Jérome Perrin committed
871
        column_group_by_expression_list.append('mirror_section_uid')
Sebastien Robin committed
872
      if group_by_payment:
Jérome Perrin committed
873
        column_group_by_expression_list.append('payment_uid')
Sebastien Robin committed
874
      if group_by_sub_variation:
Jérome Perrin committed
875
        column_group_by_expression_list.append('sub_variation_text')
Sebastien Robin committed
876
      if group_by_variation:
Jérome Perrin committed
877
        column_group_by_expression_list.append('variation_text')
Sebastien Robin committed
878
      if group_by_movement:
Jérome Perrin committed
879
        column_group_by_expression_list.append('uid')
Vincent Pelletier committed
880
      if group_by_resource:
Jérome Perrin committed
881 882 883
        column_group_by_expression_list.append('resource_uid')
      if group_by_project:
        column_group_by_expression_list.append('project_uid')
Jérome Perrin committed
884 885
      if group_by_funding:
        column_group_by_expression_list.append('funding_uid')
Nicolas Wavrant committed
886 887
      if group_by_ledger:
        column_group_by_expression_list.append('ledger_uid')
Jérome Perrin committed
888 889
      if group_by_payment_request:
        column_group_by_expression_list.append('payment_request_uid')
Jérome Perrin committed
890 891
      if group_by_function:
        column_group_by_expression_list.append('function_uid')
Jérome Perrin committed
892
      if group_by_date:
Jérome Perrin committed
893 894 895 896 897 898 899
        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
900
        related_key_select_expression_list.append('section_category_uid')
Jérome Perrin committed
901 902 903
      if group_by_section_category_strict_membership:
        related_key_group_by_expression_list.append(
            'section_category_strict_membership_uid')
Jérome Perrin committed
904 905
        related_key_select_expression_list.append(
            'section_category_strict_membership_uid')
Jérome Perrin committed
906 907
      if group_by_mirror_section_category:
        related_key_group_by_expression_list.append('mirror_section_category_uid')
Jérome Perrin committed
908
        related_key_select_expression_list.append('mirror_section_category_uid')
Jérome Perrin committed
909 910 911
      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
912 913
        related_key_select_expression_list.append(
            'mirror_section_category_strict_membership_uid')
Jérome Perrin committed
914 915
      if group_by_node_category:
        related_key_group_by_expression_list.append('node_category_uid')
Jérome Perrin committed
916
        related_key_select_expression_list.append('node_category_uid')
Jérome Perrin committed
917 918 919
      if group_by_node_category_strict_membership:
        related_key_group_by_expression_list.append(
            'node_category_strict_membership_uid')
Jérome Perrin committed
920 921
        related_key_select_expression_list.append(
            'node_category_strict_membership_uid')
Jérome Perrin committed
922 923 924 925 926
      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
927 928
        related_key_select_expression_list.append(
            'mirror_node_category_strict_membership_uid')
Jérome Perrin committed
929 930
      if group_by_payment_category:
        related_key_group_by_expression_list.append('payment_category_uid')
Jérome Perrin committed
931
        related_key_select_expression_list.append('payment_category_uid')
Jérome Perrin committed
932 933 934
      if group_by_payment_category_strict_membership:
        related_key_group_by_expression_list.append(
            'payment_category_strict_membership_uid')
Jérome Perrin committed
935 936
        related_key_select_expression_list.append(
            'payment_category_strict_membership_uid')
Jérome Perrin committed
937 938
      if group_by_function_category:
        related_key_group_by_expression_list.append('function_category_uid')
Jérome Perrin committed
939
        related_key_select_expression_list.append('function_category_uid')
Jérome Perrin committed
940 941 942
      if group_by_function_category_strict_membership:
        related_key_group_by_expression_list.append(
            'function_category_strict_membership_uid')
Jérome Perrin committed
943
        related_key_select_expression_list.append(
Jérome Perrin committed
944
            'function_category_strict_membership_uid')
Jérome Perrin committed
945 946 947 948 949 950 951 952
      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
953 954 955 956 957 958 959 960
      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
961 962 963 964 965 966 967 968
      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
969 970 971 972 973 974 975 976
      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
977

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

      return sql_kw, new_kw
Alexandre Boeglin committed
985

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

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

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

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

      resource (only in generic API in simulation)

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

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

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

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

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

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

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

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

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

      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
1036

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

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

Jérome Perrin committed
1043 1044 1045 1046
      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
1047

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

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

      simulation_state - only take rows with specified simulation_state

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

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

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

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

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

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

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

Jérome Perrin committed
1076 1077 1078 1079
      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
1080 1081 1082 1083 1084
      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
1085

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

      selection_domain, selection_report - see ListBox

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

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

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

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

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

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

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

      quantity_unit - display results using this specific quantity unit

Nicolas Dumazet committed
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
      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
1122 1123
      **kw           -  if we want extended selection with more keywords (but
                        bad performance) check what we can do with
Jean-Paul Smets committed
1124
                        buildSQLQuery
Aurel committed
1125

Jérome Perrin committed
1126 1127
      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
1128
      """
Jean-Paul Smets committed
1129 1130 1131 1132 1133 1134 1135 1136
      # 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
1137
      method = getattr(self,'get%sInventoryList' % simulation_period)
Jérome Perrin committed
1138 1139
      kw['ignore_group_by'] = 1
      result = method(inventory_list=0, src__=src__, **kw)
Jérome Perrin committed
1140
      if src__:
Alexandre Boeglin committed
1141 1142
        return result

Alexandre Boeglin committed
1143 1144
      total_result = 0.0
      if len(result) > 0:
Sebastien Robin committed
1145 1146
        if len(result) != 1:
          raise ValueError, 'Sorry we must have only one'
Nicolas Dumazet committed
1147 1148 1149 1150 1151 1152 1153 1154
        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
1155 1156

      return total_result
Jean-Paul Smets committed
1157

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

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

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

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

      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
1202
      If no group by is provided, use the default group by: movement, node and
Jérome Perrin committed
1203
      resource.
Sebastien Robin committed
1204
      """
Vincent Pelletier committed
1205
      new_group_by_dict = {}
Jérome Perrin committed
1206
      if not ignore_group_by and group_by is None:
Vincent Pelletier committed
1207
        if group_by_node or group_by_mirror_node or group_by_section or \
Nicolas Wavrant committed
1208 1209
           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
1210
           group_by_sub_variation or group_by_variation or \
Jérome Perrin committed
1211 1212
           group_by_movement or group_by_date or group_by_section_category or\
           group_by_section_category_strict_membership:
Vincent Pelletier committed
1213 1214
          if group_by_resource is None:
            group_by_resource = 1
Vincent Pelletier committed
1215
          new_group_by_dict['group_by_resource'] = group_by_resource
Vincent Pelletier committed
1216
        elif group_by_resource is None:
Vincent Pelletier committed
1217 1218 1219 1220
          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
1221

Aurel committed
1222
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud committed
1223
                              'getInventoryList')
Vincent Pelletier committed
1224 1225
    def getInventoryList(self, src__=0, optimisation__=True,
                         ignore_variation=0, standardise=0,
Vincent Pelletier committed
1226
                         omit_simulation=0,
Jérome Perrin committed
1227
                         only_accountable=True,
Sebastien Robin committed
1228
                         default_stock_table='stock',
Vincent Pelletier committed
1229
                         statistic=0, inventory_list=1,
Julien Muchembled committed
1230
                         precision=None, connection_id=None,
Nicolas Dumazet committed
1231
                         **kw):
Alexandre Boeglin committed
1232
      """
Aurel committed
1233 1234 1235
        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
1236
        a given group of resource, node, section.
Aurel committed
1237 1238
        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
1239
        average, cost, etc.)
Vincent Pelletier committed
1240 1241 1242 1243 1244 1245 1246 1247 1248 1249

        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.
1250
        Third query: Classic stock table read.
Vincent Pelletier committed
1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263
          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
1264
         -
Alexandre Boeglin committed
1265
      """
Nicolas Dumazet committed
1266 1267 1268 1269
      getCategory = self.getPortalObject().portal_categories.getCategoryUid

      result_column_id_dict = {}

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

Nicolas Dumazet committed
1274 1275 1276 1277 1278 1279
      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
1280 1281 1282
            if metric_type is None:
              # use the default metric type
              metric_type = quantity_unit.split("/", 1)[0]
Kazuhiko Shiozaki committed
1283
        elif isinstance(quantity_unit, (int, float)):
Nicolas Dumazet committed
1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297
          # 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
1298 1299
      if src__:
        sql_source_list = []
Sebastien Robin committed
1300
      # If no group at all, give a default sort group by
Vincent Pelletier committed
1301
      kw.update(self._getDefaultGroupByParameters(**kw))
Aurel committed
1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317
      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
1318
              optimisation__ and (not kw.get('from_date')) and \
Aurel committed
1319 1320 1321 1322 1323 1324 1325 1326 1327 1328
              '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
1329
          to_date = kw.pop("to_date", None)
Aurel committed
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348
        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
1349
      sql_kw, new_kw = self._generateKeywordDict(**kw)
Aurel committed
1350 1351 1352 1353 1354 1355 1356 1357 1358
      # 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
1359
      stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
Aurel committed
1360 1361 1362 1363
          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
1364
      if src__:
Aurel committed
1365
        sql_source_list.append(delta_result)
Vincent Pelletier committed
1366
        result = ';\n-- NEXT QUERY\n'.join(sql_source_list)
Aurel committed
1367 1368 1369 1370 1371
      else:
        if cached_result:
            result = self._addBrainResults(delta_result, cached_result, new_kw)
        else:
            result = delta_result
Vincent Pelletier committed
1372
      return result
Romain Courteaud committed
1373

Kazuhiko Shiozaki committed
1374 1375
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInventoryCacheLag')
Aurel committed
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 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418
    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",
              error=exc_info())
          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
1419 1420 1421
      inventory_cache_kw = {'query': sql_text_hash}
      if to_date is not None:
        inventory_cache_kw['date'] = to_date
Aurel committed
1422 1423
      try:
          cached_sql_result = Resource_zGetInventoryCacheResult(**inventory_cache_kw)
Sebastien Robin committed
1424 1425 1426
      except ProgrammingError, (code, _):
          if code != NO_SUCH_TABLE:
            raise
Aurel committed
1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451
          # 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
1452
      if cached_sql_result and (to_date is None or (to_date - DateTime(cached_sql_result[0].date) < cache_lag)):
Aurel committed
1453 1454
        cached_date = DateTime(cached_sql_result[0].date)
        result = cached_result