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.Comment
72
                      , PropertySheet.FlowCapacity
Sebastien Robin's avatar
Sebastien Robin committed
73
                      , PropertySheet.VariationRange
74
                      , PropertySheet.DefaultSupply
75
                      , PropertySheet.Aggregated
Jean-Paul Smets's avatar
Jean-Paul Smets committed
76 77 78 79 80 81 82
                      )

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

          resource.getVariationRangeCategoryItemList
            => [(display, value)]
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
        
      ## 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
115
        """
116
        result = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
117
        if base_category_list is ():
118 119 120
          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
121
          base_category_list = (base_category_list,)
122

123 124 125 126 127
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList(),
            sort_on=[('title','ascending')])
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
128
        other_base_category_dict = dict([(i,1) for i in base_category_list])
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
 
        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]
147
        # Get category variation
148 149
        if other_base_category_list:
          result.extend(Variated.getVariationRangeCategoryItemList(
150
              self, base_category_list=other_base_category_list,
151
              base=base, display_base_category=display_base_category, **kw))
152
        # Return result
153
        return result
154

Jean-Paul Smets's avatar
Jean-Paul Smets committed
155 156
    security.declareProtected(Permissions.AccessContentsInformation,
                                           'getVariationCategoryItemList')
157
    def getVariationCategoryItemList(self, base_category_list=(), 
158
                                     omit_optional_variation=0,
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
                                     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)
      """
174 175 176 177
      base_category_list = base_category_list or \
          self.getVariationBaseCategoryList()
      
      individual_bc_list = self.getIndividualVariationBaseCategoryList()
178 179 180 181 182 183 184 185 186 187
      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]
              
188
      
189
      result = Variated.getVariationCategoryItemList(self, 
190
                            base_category_list=other_bc_list, 
191
                            display_base_category=display_base_category, 
192
                            display_id=display_id, base=base, **kw)
193
      
194
      if not omit_individual_variation:
195 196 197 198
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList())
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
199

200 201
        for variation in individual_variation_list:
          for base_category in variation.getVariationBaseCategoryList():
202 203 204 205 206 207
            # 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):
208 209 210 211
              # XXX append object, relative_url ?
              # XXX now, call Renderer a lot of time.
              # Better implementation needed
              result.extend(Renderer(
212 213 214 215 216
                  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]))
217 218 219
      return result

    security.declareProtected(Permissions.AccessContentsInformation,
220
                              'getVariationCategoryList')
221
    def getVariationCategoryList(self, default=[], base_category_list=(),
222
                                 omit_individual_variation=1, **kw):
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 251
      """
        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(
252 253
                    base_category_list=base_category_list,
                    omit_individual_variation=omit_individual_variation,**kw)
254
      return [x[1] for x in vcil]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
255 256 257 258 259 260 261

# 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
262
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
263 264 265 266 267 268 269 270
        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
271
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
272 273 274 275 276 277 278 279 280 281
        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
282
    ####################################################
Jean-Paul Smets's avatar
Jean-Paul Smets committed
283
    # Stock Management
Romain Courteaud's avatar
Romain Courteaud committed
284 285 286
    ####################################################
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventory')
287
    def getInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
288
      """
289
      Returns inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
290
      """
291 292 293
      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
294

Romain Courteaud's avatar
Romain Courteaud committed
295 296
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventory')
297
    def getCurrentInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
298
      """
299
      Returns current inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
300
      """
301 302 303
      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
304

Romain Courteaud's avatar
Romain Courteaud committed
305 306
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getAvailableInventory')
307
    def getAvailableInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
308
      """
309 310
      Returns available inventory
      (current inventory - deliverable)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
311
      """
312 313 314
      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
315

Romain Courteaud's avatar
Romain Courteaud committed
316 317
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventory')
318
    def getFutureInventory(self, **kw):
319 320 321
      """
      Returns inventory at infinite
      """
322 323 324
      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
325

Romain Courteaud's avatar
Romain Courteaud committed
326 327
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryList')
328 329 330 331
    def getInventoryList(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
332 333 334
      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
335

Romain Courteaud's avatar
Romain Courteaud committed
336 337
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryList')
338
    def getCurrentInventoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
339
      """
340
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
341
      """
342 343 344 345 346 347 348 349 350 351 352 353 354
      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
355

Romain Courteaud's avatar
Romain Courteaud committed
356 357
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryList')
358
    def getFutureInventoryList(self, **kw):
359 360 361
      """
      Returns list of inventory grouped by section or site
      """
362 363 364
      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
365

Romain Courteaud's avatar
Romain Courteaud committed
366 367
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryStat')
368
    def getInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
369
      """
370
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
371
      """
372 373 374
      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
375

Romain Courteaud's avatar
Romain Courteaud committed
376 377
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryStat')
378
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
379
      """
380
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
381
      """
382 383 384 385 386 387 388 389 390 391 392 393 394
      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
395

Romain Courteaud's avatar
Romain Courteaud committed
396 397
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryStat')
398
    def getFutureInventoryStat(self, **kw):
399 400 401
      """
      Returns statistics of inventory grouped by section or site
      """
402 403 404
      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
405

Romain Courteaud's avatar
Romain Courteaud committed
406 407
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryChart')
408
    def getInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
409
      """
410
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
411
      """
412 413 414
      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
415

Romain Courteaud's avatar
Romain Courteaud committed
416 417
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryChart')
418
    def getCurrentInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
419
      """
420
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421
      """
422 423 424
      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
425

Romain Courteaud's avatar
Romain Courteaud committed
426 427
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryChart')
428
    def getFutureInventoryChart(self, **kw):
429 430 431
      """
      Returns list of inventory grouped by section or site
      """
432 433 434
      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
435

Romain Courteaud's avatar
Romain Courteaud committed
436 437
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryList')
438
    def getInventoryHistoryList(self, **kw):
439 440 441
      """
      Returns list of inventory grouped by section or site
      """
442 443 444
      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
445

Romain Courteaud's avatar
Romain Courteaud committed
446 447
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryChart')
448
    def getInventoryHistoryChart(self, **kw):
449 450 451
      """
      Returns list of inventory grouped by section or site
      """
452 453 454
      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
455

456 457 458 459
    # XXX FIXME
    # Method getCurrentMovementHistoryList, 
    # getAvailableMovementHistoryList, getFutureMovementHistoryList
    # can be added
Romain Courteaud's avatar
Romain Courteaud committed
460 461
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryList')
462
    def getMovementHistoryList(self, **kw):
463 464 465
      """
      Returns list of inventory grouped by section or site
      """
466 467 468
      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
469

Romain Courteaud's avatar
Romain Courteaud committed
470 471
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryStat')
472
    def getMovementHistoryStat(self, **kw):
473 474 475
      """
      Returns list of inventory grouped by section or site
      """
476 477 478
      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
479

Romain Courteaud's avatar
Romain Courteaud committed
480 481
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getNextNegativeInventoryDate')
482
    def getNextNegativeInventoryDate(self, **kw):
483 484 485
      """
      Returns list of inventory grouped by section or site
      """
486 487 488
      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
489 490


491
    # Asset Price API
492 493
    security.declareProtected(Permissions.AccessContentsInformation,
        'getInventoryAssetPrice')
494
    def getInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
495
      """
496
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
497
      """
498 499 500
      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
501

502 503
    security.declareProtected(Permissions.AccessContentsInformation,
        'getCurrentInventoryAssetPrice')
504
    def getCurrentInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
505
      """
506
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
507
      """
508 509 510
      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
511

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

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

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

533
    # Industrial price API
534 535
    security.declareProtected(Permissions.AccessContentsInformation,
        'getIndustrialPrice')
536 537 538 539 540 541 542 543 544 545 546 547
    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

548
    # Predicate handling
549 550
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'asPredicate')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
551 552 553 554 555 556 557
    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',))
558 559
      p.setMembershipCriterionCategoryList(('resource/%s'
          % self.getRelativeUrl(),))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
560
      return p
561

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

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

Alexandre Boeglin's avatar
Alexandre Boeglin committed
591 592 593 594 595 596 597 598 599 600 601
      # 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)
602 603 604
      # Get price parameters
      price_parameter_dict = {
        'base_price': None,
605 606 607
        'additional_price': [],
        'surcharge_ratio': [],
        'discount_ratio': [],
608
        'exclusive_discount_ratio': None,
609 610
        'variable_additional_price': [],
        'non_discountable_additional_price': [],
611
        'priced_quantity': None,
612
      }
Alexandre Boeglin's avatar
Alexandre Boeglin committed
613 614 615 616
      if mapped_value is None:
        return price_parameter_dict
      for price_parameter_name in price_parameter_dict.keys():
        price_parameter_value = \
617 618 619
          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
620 621 622 623 624 625
          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':
626
                price_parameter_dict[price_parameter_name] = \
Alexandre Boeglin's avatar
Alexandre Boeglin committed
627 628 629 630
                    max(price_parameter_value)
              else:
                price_parameter_dict[price_parameter_name] = \
                    price_parameter_value[0]
631 632
      return price_parameter_dict
      
633
    security.declareProtected(Permissions.AccessContentsInformation,
634 635
        'getPricingVariable')
    def getPricingVariable(self, context=None):
636 637 638 639
      """
      Return the value of the property used to calculate variable pricing
      This basically calls a script like Product_getPricingVariable
      """
640 641 642
      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)
643
      method = None
644
      if context is not None:
645
        method = context._getTypeBasedMethod('getPricingVariable')
646
      if method is None or context is None:
647
        method = self._getTypeBasedMethod('getPricingVariable')
648

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

653
    security.declareProtected(Permissions.AccessContentsInformation, 
654 655 656 657 658 659 660 661
                              '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.
662 663
      # Note that this is based on self (i.e. a resource) instead of context
      # (i.e. a movement).
664
      method = self._getTypeBasedMethod('getPriceCalculationOperandDict')
665
      if method is not None:
666
        return method(default=default, movement=context, REQUEST=REQUEST, **kw)
667

668 669 670 671 672 673 674 675 676
      # 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
677 678 679 680 681
      # 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.

682
      price_parameter_dict = self.getPriceParameterDict(
683 684 685
                                     context=context, REQUEST=REQUEST, **kw)
      # Calculate the unit price
      unit_base_price = None
686
      # Calculate
687 688 689 690 691
#     ((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))
692 693 694 695 696
      # Or, as (nearly) one single line :
#     ((bp + S(ap) + v * S(vap))
#       * (1 - m(1, M(S(dr), edr)))
#       + S(ndap))
#     * (1 + S(sr))
697 698 699
      # 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
700 701
      # depends on discrete variations, but also on a continuous property
      # of the object
702

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

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


    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
798
      quantity = float(traverse(quantity_unit).getProperty('quantity'))
799 800 801 802
      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
803
        quantity /= float(traverse(management_unit).getProperty('quantity'))
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
      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.
891
        quantity = quantity_unit_value.getProperty('quantity')
892 893 894 895
        metric_type_uid = self.getPortalObject().portal_categories \
                              .getCategoryUid(metric_type, 'metric_type')
        if quantity and metric_type_uid:
          uid = self.getUid()
896
          insert_list += (uid, uid, '^', metric_type_uid, float(quantity)),
897 898

      return insert_list