Resource.py 37.2 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
3
# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
5
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
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.
#
##############################################################################

30
from math import log
Alexandre Boeglin's avatar
Alexandre Boeglin committed
31
from warnings import warn
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32

33
from AccessControl import ClassSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34

35
from Products.ERP5Type import Permissions, PropertySheet, Interface
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36
from Products.ERP5Type.XMLMatrix import XMLMatrix
37
from Products.ERP5Type.Base import Base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38 39

from Products.ERP5.Variated import Variated
40
from Products.CMFCategory.Renderer import Renderer
41
from Products.CMFCore.utils import getToolByName
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42

Alexandre Boeglin's avatar
Alexandre Boeglin committed
43
from zLOG import LOG, WARNING
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44

45
class Resource(XMLMatrix, Variated):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
46 47 48 49 50 51
    """
      A Resource
    """

    meta_type = 'ERP5 Resource'
    portal_type = 'Resource'
52
    add_permission = Permissions.AddPortalContent
Jean-Paul Smets's avatar
Jean-Paul Smets committed
53 54 55 56 57
    isPortalContent = 1
    isRADContent = 1

    # Declarative security
    security = ClassSecurityInfo()
58
    security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
59 60 61 62 63 64 65 66 67 68 69 70

    # Declarative interfaces
    __implements__ = ( Interface.Variated, )

    # Declarative properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.XMLObject
                      , PropertySheet.CategoryCore
                      , PropertySheet.DublinCore
                      , PropertySheet.Price
                      , PropertySheet.Resource
                      , PropertySheet.Reference
71
                      , PropertySheet.FlowCapacity
Sebastien Robin's avatar
Sebastien Robin committed
72
                      , PropertySheet.VariationRange
73
                      , PropertySheet.DefaultSupply
74
                      , PropertySheet.Aggregated
Jean-Paul Smets's avatar
Jean-Paul Smets committed
75 76 77 78 79 80 81
                      )

    # Is it OK now ?
    # The same method is at about 3 different places
    # Some genericity is needed
    security.declareProtected(Permissions.AccessContentsInformation,
                                           'getVariationRangeCategoryItemList')
82 83 84
    def getVariationRangeCategoryItemList(self, base_category_list=(), base=1, 
                                          root=1, display_id='title', 
                                          display_base_category=1,
85 86
                                          current_category=None,
                                          omit_individual_variation=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
87 88
        """
          Returns possible variations
89 90 91

          resource.getVariationRangeCategoryItemList
            => [(display, value)]
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        
      ## Variation API (exemple) ##
        Base categories defined:
          - colour
          - morphology
          - size
        Categories defined:
          - colour/blue
          - colour/red
          - size/Man
          - size/Woman
        Resource 'resource' created with variation_base_category_list:
            (colour, morphology, size)

        resource.getVariationRangeCategoryList
        variation   | individual variation | result
        ____________________________________________________________________________________
                    |                      | (colour/blue, colour/red, size/Man, size/Woman)
        size/Man    |                      | (colour/blue, colour/red, size/Man, size/Woman)
        colour/blue |                      | (colour/blue, colour/red, size/Man, size/Woman)
                    |  colour/1            | (colour/1, size/Man, size/Woman)
                    |  morphology/2        | (colour/blue, colour/red, size/Man, size/Woman, morphology/2)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
114
        """
115
        result = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
116
        if base_category_list is ():
117 118 119
          base_category_list = self.getVariationBaseCategoryList(
              omit_individual_variation=omit_individual_variation)
        elif isinstance(base_category_list, str):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
120
          base_category_list = (base_category_list,)
121

122 123 124 125 126
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList(),
            sort_on=[('title','ascending')])
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
127
        other_base_category_dict = dict([(i,1) for i in base_category_list])
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
 
        if not omit_individual_variation:              
          for variation in individual_variation_list:
            for base_category in variation.getVariationBaseCategoryList():
              if base_category_list is ()\
                  or base_category in base_category_list:
                other_base_category_dict[base_category] = 0
                # XXX now, call Renderer a lot of time.
                # Better implementation needed
                result.extend(Renderer(
                    base_category=base_category, 
                    display_base_category=display_base_category,
                    display_none_category=0, base=base,
                    current_category=current_category,
                    display_id=display_id).render([variation]))

        other_base_category_list = [x for x, y in
            other_base_category_dict.iteritems() if y == 1]
146
        # Get category variation
147 148
        if other_base_category_list:
          result.extend(Variated.getVariationRangeCategoryItemList(
149
              self, base_category_list=other_base_category_list,
150
              base=base, display_base_category=display_base_category, **kw))
151
        # Return result
152
        return result
153

Jean-Paul Smets's avatar
Jean-Paul Smets committed
154 155
    security.declareProtected(Permissions.AccessContentsInformation,
                                           'getVariationCategoryItemList')
156
    def getVariationCategoryItemList(self, base_category_list=(), 
157
                                     omit_optional_variation=0,
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
                                     omit_individual_variation=1, base=1,
                                     current_category=None,
                                     display_base_category=1,
                                     display_id='title', **kw):
      """
        Returns variations of the resource.
        If omit_individual_variation==1, does not return individual 
        variation.
        Else, returns them.
        Display is on left.
            => [(display, value)]

        *old parameters: base=1, current_category=None, 
                         display_id='getTitle' (default value getTitleOrId)
      """
173 174 175 176
      base_category_list = base_category_list or \
          self.getVariationBaseCategoryList()
      
      individual_bc_list = self.getIndividualVariationBaseCategoryList()
177 178 179 180 181 182 183 184 185 186
      other_bc_list = [x for x in base_category_list
          if not x in individual_bc_list]

      if omit_optional_variation:
        optional_bc_list = self.getOptionalVariationBaseCategoryList()\
            or self.getPortalOptionBaseCategoryList()
        if optional_bc_list:
          other_bc_list = [x for x in other_bc_list
              if not x in optional_bc_list]
              
187
      
188
      result = Variated.getVariationCategoryItemList(self, 
189
                            base_category_list=other_bc_list, 
190
                            display_base_category=display_base_category, 
191
                            display_id=display_id, base=base, **kw)
192
      
193
      if not omit_individual_variation:
194 195 196 197
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList())
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
198

199 200
        for variation in individual_variation_list:
          for base_category in variation.getVariationBaseCategoryList():
201 202 203 204 205 206
            # backwards compatbility: if individual_bc_list is empty, allow
            # all individual variation base categories.
            if (base_category_list is ()
                or base_category in base_category_list)\
               and (not len(individual_bc_list)
                    or base_category in individual_bc_list):
207 208 209 210
              # XXX append object, relative_url ?
              # XXX now, call Renderer a lot of time.
              # Better implementation needed
              result.extend(Renderer(
211 212 213 214 215
                  base_category=base_category,
                  display_base_category=display_base_category,
                  display_none_category=0, base=base,
                  current_category=current_category, display_id=display_id,
                  **kw).render([variation]))
216 217 218
      return result

    security.declareProtected(Permissions.AccessContentsInformation,
219
                              'getVariationCategoryList')
220
    def getVariationCategoryList(self, default=[], base_category_list=(),
221
                                 omit_individual_variation=1, **kw):
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
      """
        Returns variations of the resource.
        If omit_individual_variation==1, does not return individual 
        variation.
        Else, returns them.

        ## Variation API (exemple) ##
        Base categories defined:
          - colour
          - morphology
          - size
        Categories defined:
          - colour/blue
          - colour/red
          - size/Man
          - size/Woman
        Resource 'resource' created with variation_base_category_list:
            (colour, morphology, size)

        resource.getVariationCategoryList
        variation   | individual variation | result
        _____________________________________________________
                    |                      | ()
        size/Man    |                      | (size/Man, )
        colour/blue |                      | (colour/blue, )
                    |  colour/1            | (colour/1, )
                    |  morphology/2        | (morphology/2, )
      """
      vcil = self.getVariationCategoryItemList(
251 252
                    base_category_list=base_category_list,
                    omit_individual_variation=omit_individual_variation,**kw)
253
      return [x[1] for x in vcil]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
254 255 256 257 258 259 260

# This patch is temporary and allows to circumvent name conflict in ZSQLCatalog process for Coramy
    security.declareProtected(Permissions.AccessContentsInformation,
                                              'getDefaultDestinationAmountBis')
    def getDefaultDestinationAmountBis(self, unit=None, variation=None, REQUEST=None):
      try:
        return self.getDestinationReference()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
261
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
262 263 264 265 266 267 268 269
        return None

# This patch is temporary and allows to circumvent name conflict in ZSQLCatalog process for Coramy
    security.declareProtected(Permissions.AccessContentsInformation,
                                              'getDefaultSourceAmountBis')
    def getDefaultSourceAmountBis(self, unit=None, variation=None, REQUEST=None):
      try:
        return self.getSourceReference()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
270
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
271 272 273 274 275 276 277 278 279 280
        return None


    # This patch allows variations to find a resource
    security.declareProtected(Permissions.AccessContentsInformation,
                                              'getDefaultResourceValue')
    def getDefaultResourceValue(self):
      return self


Romain Courteaud's avatar
Romain Courteaud committed
281
    ####################################################
Jean-Paul Smets's avatar
Jean-Paul Smets committed
282
    # Stock Management
Romain Courteaud's avatar
Romain Courteaud committed
283 284 285
    ####################################################
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventory')
286
    def getInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
287
      """
288
      Returns inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
289
      """
290 291 292
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
293

Romain Courteaud's avatar
Romain Courteaud committed
294 295
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventory')
296
    def getCurrentInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
297
      """
298
      Returns current inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
299
      """
300 301 302
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getCurrentInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
303

Romain Courteaud's avatar
Romain Courteaud committed
304 305
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getAvailableInventory')
306
    def getAvailableInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
307
      """
308 309
      Returns available inventory
      (current inventory - deliverable)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
310
      """
311 312 313
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getAvailableInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
314

Romain Courteaud's avatar
Romain Courteaud committed
315 316
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventory')
317
    def getFutureInventory(self, **kw):
318 319 320
      """
      Returns inventory at infinite
      """
321 322 323
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
324

Romain Courteaud's avatar
Romain Courteaud committed
325 326
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryList')
327 328 329 330
    def getInventoryList(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
331 332 333
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
334

Romain Courteaud's avatar
Romain Courteaud committed
335 336
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryList')
337
    def getCurrentInventoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
338
      """
339
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
340
      """
341 342 343 344 345 346 347 348 349 350 351 352 353
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getCurrentInventoryList(**kw)

    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getAvailableInventoryList')
    def getAvailableInventoryList(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getAvailableInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
354

Romain Courteaud's avatar
Romain Courteaud committed
355 356
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryList')
357
    def getFutureInventoryList(self, **kw):
358 359 360
      """
      Returns list of inventory grouped by section or site
      """
361 362 363
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
364

Romain Courteaud's avatar
Romain Courteaud committed
365 366
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryStat')
367
    def getInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
368
      """
369
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
370
      """
371 372 373
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
374

Romain Courteaud's avatar
Romain Courteaud committed
375 376
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryStat')
377
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
378
      """
379
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
380
      """
381 382 383 384 385 386 387 388 389 390 391 392 393
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getCurrentInventoryStat(**kw)

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getAvailableInventoryStat')
    def getAvailableInventoryStat(self, **kw):
      """
      Returns statistics of inventory grouped by section or site
      """
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getAvailableInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
394

Romain Courteaud's avatar
Romain Courteaud committed
395 396
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryStat')
397
    def getFutureInventoryStat(self, **kw):
398 399 400
      """
      Returns statistics of inventory grouped by section or site
      """
401 402 403
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
404

Romain Courteaud's avatar
Romain Courteaud committed
405 406
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryChart')
407
    def getInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
408
      """
409
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
410
      """
411 412 413
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
414

Romain Courteaud's avatar
Romain Courteaud committed
415 416
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryChart')
417
    def getCurrentInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
418
      """
419
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
420
      """
421 422 423
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getCurrentInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
424

Romain Courteaud's avatar
Romain Courteaud committed
425 426
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryChart')
427
    def getFutureInventoryChart(self, **kw):
428 429 430
      """
      Returns list of inventory grouped by section or site
      """
431 432 433
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
434

Romain Courteaud's avatar
Romain Courteaud committed
435 436
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryList')
437
    def getInventoryHistoryList(self, **kw):
438 439 440
      """
      Returns list of inventory grouped by section or site
      """
441 442 443
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
444

Romain Courteaud's avatar
Romain Courteaud committed
445 446
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryChart')
447
    def getInventoryHistoryChart(self, **kw):
448 449 450
      """
      Returns list of inventory grouped by section or site
      """
451 452 453
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryHistoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
454

455 456 457 458
    # XXX FIXME
    # Method getCurrentMovementHistoryList, 
    # getAvailableMovementHistoryList, getFutureMovementHistoryList
    # can be added
Romain Courteaud's avatar
Romain Courteaud committed
459 460
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryList')
461
    def getMovementHistoryList(self, **kw):
462 463 464
      """
      Returns list of inventory grouped by section or site
      """
465 466 467
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getMovementHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
468

Romain Courteaud's avatar
Romain Courteaud committed
469 470
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryStat')
471
    def getMovementHistoryStat(self, **kw):
472 473 474
      """
      Returns list of inventory grouped by section or site
      """
475 476 477
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getMovementHistoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
478

Romain Courteaud's avatar
Romain Courteaud committed
479 480
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getNextNegativeInventoryDate')
481
    def getNextNegativeInventoryDate(self, **kw):
482 483 484
      """
      Returns list of inventory grouped by section or site
      """
485 486 487
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getNextNegativeInventoryDate(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
488 489


490
    # Asset Price API
491 492
    security.declareProtected(Permissions.AccessContentsInformation,
        'getInventoryAssetPrice')
493
    def getInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
494
      """
495
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
496
      """
497 498 499
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getInventoryAssetPrice(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
500

501 502
    security.declareProtected(Permissions.AccessContentsInformation,
        'getCurrentInventoryAssetPrice')
503
    def getCurrentInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
504
      """
505
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
506
      """
507 508 509
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getCurrentInventoryAssetPrice(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
510

511 512
    security.declareProtected(Permissions.AccessContentsInformation,
        'getAvailableInventoryAssetPrice')
513
    def getAvailableInventoryAssetPrice(self, **kw):
514 515 516
      """
      Returns list of inventory grouped by section or site
      """
517 518 519
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getAvailableInventoryAssetPrice(**kw)
520

521 522
    security.declareProtected(Permissions.AccessContentsInformation,
        'getFutureInventoryAssetPrice')
523
    def getFutureInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
524
      """
525
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
526
      """
527 528 529
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventoryAssetPrice(**kw)
530

Jean-Paul Smets's avatar
Jean-Paul Smets committed
531

532
    # Industrial price API
533 534
    security.declareProtected(Permissions.AccessContentsInformation,
        'getIndustrialPrice')
535 536 537 538 539 540 541 542 543 544 545 546
    def getIndustrialPrice(self, context=None, REQUEST=None, **kw):
      """
        Returns industrial price
      """
      context = self.asContext(context=context, REQUEST=REQUEST, **kw)
      result = self._getIndustrialPrice(context)
      return result

    def _getIndustrialPrice(self, context):
      # Default value is None
      return None

547
    # Predicate handling
548 549
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'asPredicate')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
550 551 552 553 554 555 556
    def asPredicate(self):
      """
      Returns a temporary Predicate based on the Resource properties
      """
      from Products.ERP5 import newTempPredicateGroup as newTempPredicate
      p = newTempPredicate(self.getId(), uid = self.getUid())
      p.setMembershipCriterionBaseCategoryList(('resource',))
557 558
      p.setMembershipCriterionCategoryList(('resource/%s'
          % self.getRelativeUrl(),))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
559
      return p
560

561
    def _pricingSortMethod(self, a, b):
Alexandre Boeglin's avatar
Alexandre Boeglin committed
562
      # Simple method : the one that defines a destination wins
563 564 565
      if a.getDestination():
        return -1 # a defines a destination and wins
      return 1 # a defines no destination ans loses
566

567
    security.declareProtected(Permissions.AccessContentsInformation, 
568
                              'getPriceParameterDict')
569
    def getPriceParameterDict(self, context=None, REQUEST=None, **kw):
570
      """
571
      Get all pricing parameters from Predicate.
572
      """
573
      # Search all categories context
574
      new_category_list = []
575
      if context is not None:
576
        new_category_list += context.getCategoryList()
577 578
      #XXX This should be 'category_list' instead of 'categories' to respect
      # the naming convention. Must take care of side effects when fixing
579 580 581
      if kw.has_key('categories'):
        new_category_list.extend(kw['categories'])
        del kw['categories']
582 583
      resource_category = 'resource/' + self.getRelativeUrl()
      if not resource_category in new_category_list:
584 585 586
        new_category_list += (resource_category, )
      # Generate the predicate mapped value
      # to get some price values.
587
      domain_tool = getToolByName(self,'portal_domains')
588
      portal_type_list = self.getPortalSupplyPathTypeList()
589

Alexandre Boeglin's avatar
Alexandre Boeglin committed
590 591 592 593 594 595 596 597 598 599 600
      # Generate the fake context
      tmp_context = self.asContext(context=context, 
                                   categories=new_category_list,
                                   REQUEST=REQUEST, **kw)
      tmp_kw = kw.copy()
      if 'sort_method' not in tmp_kw:
        tmp_kw['sort_method'] = self._pricingSortMethod
      mapped_value = domain_tool.generateMultivaluedMappedValue(
                                             tmp_context,
                                             portal_type=portal_type_list,
                                             has_cell_content=0, **tmp_kw)
601 602 603
      # Get price parameters
      price_parameter_dict = {
        'base_price': None,
604 605 606
        'additional_price': [],
        'surcharge_ratio': [],
        'discount_ratio': [],
607
        'exclusive_discount_ratio': None,
608 609
        'variable_additional_price': [],
        'non_discountable_additional_price': [],
610
        'priced_quantity': None,
611
      }
Alexandre Boeglin's avatar
Alexandre Boeglin committed
612 613 614 615
      if mapped_value is None:
        return price_parameter_dict
      for price_parameter_name in price_parameter_dict.keys():
        price_parameter_value = \
616 617 618
          mapped_value.getProperty(price_parameter_name,
              d=price_parameter_dict[price_parameter_name])
        if price_parameter_value not in [None, [], '']:
Alexandre Boeglin's avatar
Alexandre Boeglin committed
619 620 621 622 623 624
          try:
            price_parameter_dict[price_parameter_name].extend(
                                            price_parameter_value)
          except AttributeError:
            if price_parameter_dict[price_parameter_name] is None:
              if price_parameter_name == 'exclusive_discount_ratio':
625
                price_parameter_dict[price_parameter_name] = \
Alexandre Boeglin's avatar
Alexandre Boeglin committed
626 627 628 629
                    max(price_parameter_value)
              else:
                price_parameter_dict[price_parameter_name] = \
                    price_parameter_value[0]
630 631
      return price_parameter_dict
      
632
    security.declareProtected(Permissions.AccessContentsInformation,
633 634
        'getPricingVariable')
    def getPricingVariable(self, context=None):
635 636 637 638
      """
      Return the value of the property used to calculate variable pricing
      This basically calls a script like Product_getPricingVariable
      """
639 640 641
      warn('Resource.getPricingVariable is deprecated; Please use' \
           ' a type-based method for getPrice, and call whatever scripts' \
           ' you need to invoke from that method.', DeprecationWarning)
642
      method = None
643
      if context is not None:
644
        method = context._getTypeBasedMethod('getPricingVariable')
645
      if method is None or context is None:
646
        method = self._getTypeBasedMethod('getPricingVariable')
647

648 649 650 651
      if method is None:
        return 0.0
      return float(method())

652
    security.declareProtected(Permissions.AccessContentsInformation, 
653 654 655 656 657 658 659 660
                              'getPriceCalculationOperandDict')
    def getPriceCalculationOperandDict(self, default=None, context=None,
            REQUEST=None, **kw):
      """Return a dictionary which contains operands for price calculation.
      Consult the doc string in Movement.getPriceCalculationOperandDict
      for more details.
      """
      # First, try to use a new type-based method for the calculation.
661 662
      # Note that this is based on self (i.e. a resource) instead of context
      # (i.e. a movement).
663
      method = self._getTypeBasedMethod('getPriceCalculationOperandDict')
664
      if method is not None:
665
        return method(default=default, movement=context, REQUEST=REQUEST, **kw)
666

667 668 669 670 671 672 673 674 675
      # Next, try an old type-based method which returns only a final result.
      method = self._getTypeBasedMethod('getPrice')
      if method is not None:
        price = method(default=default, movement=context, REQUEST=REQUEST, **kw)
        if price is not None:
          return {'price': price}
        return default

      # This below is used only if any type-based method is not
676 677 678 679 680
      # available at all. We should provide the default implementation
      # in a Business Template as Resource_getPrice, thus this will not
      # be used in the future. Kept only for backward compatibility in
      # case where the user still uses an older Business Template.

681
      price_parameter_dict = self.getPriceParameterDict(
682 683 684
                                     context=context, REQUEST=REQUEST, **kw)
      # Calculate the unit price
      unit_base_price = None
685
      # Calculate
686 687 688 689 690
#     ((base_price + SUM(additional_price) +
#     variable_value * SUM(variable_additional_price)) *
#     (1 - MIN(1, MAX(SUM(discount_ratio) , exclusive_discount_ratio ))) +
#     SUM(non_discountable_additional_price)) *
#     (1 + SUM(surcharge_ratio))
691 692 693 694 695
      # Or, as (nearly) one single line :
#     ((bp + S(ap) + v * S(vap))
#       * (1 - m(1, M(S(dr), edr)))
#       + S(ndap))
#     * (1 + S(sr))
696 697 698
      # Variable value is dynamically configurable through a python script.
      # It can be anything, depending on business requirements.
      # It can be seen as a way to define a pricing model that not only
699 700
      # depends on discrete variations, but also on a continuous property
      # of the object
701

702
      base_price = price_parameter_dict['base_price']
703
      if base_price in [None, '']:
704 705
        # XXX Compatibility
        # base_price must not be defined on resource
706 707
        base_price = self.getBasePrice()
      if base_price not in [None, '']:
708 709
        unit_base_price = base_price
        # Sum additional price
710
        for additional_price in price_parameter_dict['additional_price']:
711
          unit_base_price += additional_price
712
        # Sum variable additional price
713
        variable_value = self.getPricingVariable(context=context)
714 715
        for variable_additional_price in \
            price_parameter_dict['variable_additional_price']:
716
          unit_base_price += variable_additional_price * variable_value
717
        # Discount
718 719 720
        sum_discount_ratio = 0
        for discount_ratio in price_parameter_dict['discount_ratio']:
          sum_discount_ratio += discount_ratio
721 722 723
        exclusive_discount_ratio = \
            price_parameter_dict['exclusive_discount_ratio']
        d_ratio = 0
724
        d_ratio = max(d_ratio, sum_discount_ratio)
725 726 727 728 729
        if exclusive_discount_ratio not in [None, '']:
          d_ratio = max(d_ratio, exclusive_discount_ratio)
        if d_ratio != 0:
          d_ratio = 1 - min(1, d_ratio)
          unit_base_price = unit_base_price * d_ratio
730 731 732 733 734 735 736 737 738
        # Sum non discountable additional price
        for non_discountable_additional_price in\
            price_parameter_dict['non_discountable_additional_price']:
          unit_base_price += non_discountable_additional_price
        # Surcharge ratio
        sum_surcharge_ratio = 1
        for surcharge_ratio in price_parameter_dict['surcharge_ratio']:
          sum_surcharge_ratio += surcharge_ratio
        unit_base_price = unit_base_price * sum_surcharge_ratio
739 740 741
      # Divide by the priced quantity if not (None, 0)
      if unit_base_price is not None\
          and price_parameter_dict['priced_quantity']:
742
        priced_quantity = price_parameter_dict['priced_quantity']
743
        unit_base_price = unit_base_price / priced_quantity
744
      # Return result
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
      if unit_base_price is not None:
        return {'price': unit_base_price}
      return default

    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getPrice')
    def getPrice(self, default=None, context=None, REQUEST=None, **kw):
      """
      Return the unit price of a resource in a specific context.
      """
      # see Movement.getPrice
      if isinstance(default, Base) and context is None:
        msg = 'getPrice first argument is supposed to be the default value'\
              ' accessor, the context should be passed as with the context='\
              ' keyword argument'
        warn(msg, DeprecationWarning)
        LOG('ERP5', WARNING, msg)
        context = default
        default = None
      
      operand_dict = self.getPriceCalculationOperandDict(default=default, 
              context=context, REQUEST=REQUEST, **kw)
      if operand_dict is not None:
        return operand_dict['price']
      return default
Yoshinori Okuji's avatar
Yoshinori Okuji committed
770 771 772 773 774 775

    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getQuantityPrecision')
    def getQuantityPrecision(self):
      """Return the floating point precision of a quantity.
      """
776 777 778
      try:
        return int(round(- log(self.getBaseUnitQuantity(), 10),0))
      except TypeError:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
779
        return 0
780
      return 0
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796


    def _getConversionRatio(self, quantity_unit, variation_list):
      """
      Converts a quantity unit into a ratio in respect to the resource's
      management unit, for the specified variation.
      A quantity can be multiplied by the returned value in order to convert it
      in the management unit.

      'variation_list' parameter may be deprecated:
      cf Measure.getConvertedQuantity
      """
      management_unit = self.getDefaultQuantityUnit()
      if management_unit == quantity_unit:
        return 1.0
      traverse = self.portal_categories['quantity_unit'].unrestrictedTraverse
Nicolas Delaby's avatar
Nicolas Delaby committed
797
      quantity = float(traverse(quantity_unit).getProperty('quantity'))
798 799 800 801
      if quantity_unit.split('/', 1)[0] != management_unit.split('/', 1)[0]:
        measure = self.getDefaultMeasure(quantity_unit)
        quantity /= measure.getConvertedQuantity(variation_list)
      else:
Nicolas Delaby's avatar
Nicolas Delaby committed
802
        quantity /= float(traverse(management_unit).getProperty('quantity'))
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897
      return quantity

    # Unit conversion
    security.declareProtected(Permissions.AccessContentsInformation, 'convertQuantity')
    def convertQuantity(self, quantity, from_unit, to_unit, variation_list=()):
      # 'variation_list' parameter may be deprecated:
      # cf Measure.getConvertedQuantity
      try:
        return quantity * self._getConversionRatio(from_unit, variation_list) \
                        / self._getConversionRatio(to_unit, variation_list)
      except (ArithmeticError, AttributeError, LookupError, TypeError), error:
        # For compatibility, we only log the error and return None.
        # No exception for the moment.
        LOG('Resource.convertQuantity', WARNING,
            'could not convert quantity for %s (%r)'
            % (self.getRelativeUrl(), error))

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMeasureList')
    def getMeasureList(self):
      """
      Gets the list of Measure objects describing this resource.
      """
      return self.objectValues(portal_type='Measure')

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDefaultMeasure')
    def getDefaultMeasure(self, quantity_unit=None):
      """
      Returns the measure object associated to quantity_unit.
      If no quantity_unit is specified, the quantity_unit of the resource is used.
      None is returned if the number of found measures differs from 1.
      """
      if quantity_unit is None:
        quantity_unit = self.getQuantityUnit()
      if quantity_unit:
        top = lambda relative_url: relative_url.split('/', 1)[0]

        quantity = top(quantity_unit)
        generic = []
        default = []
        for measure in self.getMeasureList():
          metric_type = measure.getMetricType()
          if metric_type and quantity == top(metric_type) and \
             measure.getDefaultMetricType():
            default.append(measure)
          if quantity == metric_type:
            generic.append(measure)
        result = default or generic
        if len(result) == 1:
          return result[0]

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMeasureRowList')
    def getMeasureRowList(self):
      """
      Returns a list rows to insert in the measure table.
      Used by z_catalog_measure_list.
      """
      quantity_unit_value = self.getQuantityUnitValue()
      if quantity_unit_value is None:
        return ()

      quantity_unit = quantity_unit_value.getCategoryRelativeUrl()
      default = self.getDefaultMeasure(quantity_unit)
      if default is not None:
        default = default.getRelativeUrl()
      metric_type_map = {} # duplicate metric_type are not valid

      for measure in self.getMeasureList():
        metric_type = measure.getMetricType()
        if metric_type in metric_type_map:
          metric_type_map[metric_type] = ()
        else:
          metric_type_map[metric_type] = measure.asCatalogRowList()
        if measure.getRelativeUrl() == default:
          quantity_unit = ''

      insert_list = []
      for measure_list in metric_type_map.itervalues():
        insert_list += measure_list

      metric_type = quantity_unit.split('/', 1)[0]
      if metric_type and metric_type not in metric_type_map:
        # At this point, we know there is no default measure and we must add
        # a row for the management unit, with the resource's uid as uid, and
        # a generic metric_type.
        quantity = quantity_unit_value.getProperty('quantity')
        metric_type_uid = self.getPortalObject().portal_categories \
                              .getCategoryUid(metric_type, 'metric_type')
        if quantity and metric_type_uid:
          uid = self.getUid()
          insert_list += (uid, uid, '^', metric_type_uid, quantity),

      return insert_list