Resource.py 47 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
import zope.interface
31
from math import log
Alexandre Boeglin's avatar
Alexandre Boeglin committed
32
from warnings import warn
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33

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

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

40 41
from Products.ERP5Type.Utils import cartesianProduct

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

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

48
class Resource(XMLMatrix, Variated):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
49 50 51 52 53 54
    """
      A Resource
    """

    meta_type = 'ERP5 Resource'
    portal_type = 'Resource'
55
    add_permission = Permissions.AddPortalContent
Jean-Paul Smets's avatar
Jean-Paul Smets committed
56 57 58

    # Declarative security
    security = ClassSecurityInfo()
59
    security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
60 61

    # Declarative interfaces
62
    zope.interface.implements( interfaces.IVariated, )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63 64 65 66 67 68 69 70 71

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

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

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

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

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

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

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

# 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
263
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
264 265 266 267 268 269 270 271
        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
272
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
273 274 275 276 277 278 279 280 281 282
        return None


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


283 284
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDefaultTransformationValue')
285 286 287
    def getDefaultTransformationValue(self, context=None):
      """
      If context is None, returns the first available transformation that
288 289
      use self as a Resource. If there are several candidates, return the
      Transformation that has the latest version.
290 291 292 293 294

      Otherwise, context is used as a Predicate to match Transformations.
      If the search returns several candidates due to a relaxed Predicate,
      the first item is returned arbitrarily.
      """
295 296 297 298
      method = self._getTypeBasedMethod('getDefaultTransformationValue')
      if method is not None:
        return method(context)

299
      if context is None:
Nicolas Dumazet's avatar
Nicolas Dumazet committed
300 301 302 303 304 305
        transformation_list = self.portal_catalog(
            portal_type="Transformation",
            resource_relative_url=self.getRelativeUrl(),
            sort_on=[('version', 'descending')],
            limit=1
        )
306 307
        if len(transformation_list) > 0:
          return transformation_list[0].getObject()
308
        return None
309

310
      method = context._getTypeBasedMethod('getDefaultTransformationValue')
311 312 313
      if method is not None:
        return method(context)

314
      transformation_list = self.portal_domains.searchPredicateList(context,
315 316
                                portal_type="Transformation",
                                limit=1)
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333

      if len(transformation_list) > 0:
        return transformation_list[0]

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDefaultConversionTransformationValue')
    def getDefaultConversionTransformationValue(self):
      """
      Return a Transformation object that should be used to compute
      converted inventories.
      This should be overriden in subclasses, or in the Type Based Method
      of the same name.

      The method can return an existing Transformation object, or a
      temporary Transformation: one might want for example, for conversion
      purposes, to ignore some (packaging, wrapping, labelling) components
      in conversion reports. This method can be used to create a simplified
334
      transformation from a complex real-world transformation.
335
      """
336
      method = self._getTypeBasedMethod(\
337 338 339 340
                        'getDefaultConversionTransformationValue')
      if method is not None:
        return method()

341
      return self.getDefaultTransformationValue(context=None)
342 343


344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
    security.declareProtected(Permissions.AccessContentsInformation,
                           'getTransformationVariationCategoryCartesianProduct')
    def getTransformationVariationCategoryCartesianProduct(self):
      """
      Defines which variations are of interest when indexing
      Transformations related to this resource.

      By default, this returns the cartesian Product of all
      possible categories using all variation axes.

      Override this to reduce the number of indexed rows, and/or
      if some variation axes do not matter when displaying
      Transformed inventories.

      XXX This should use variated_range mixin when available
      """
      method = self._getTypeBasedMethod(\
          'getTransformationVariationCategoryCartesianProduct')
      if method is not None:
        return method()

      variation_list_list = []
      for base_variation in self.getVariationBaseCategoryList():
        variation_list = self.getVariationCategoryList( \
            base_category_list=(base_variation,))
        if len(variation_list) > 0:
          variation_list_list.append(variation_list)

      return cartesianProduct(variation_list_list)


Romain Courteaud's avatar
Romain Courteaud committed
375
    ####################################################
Jean-Paul Smets's avatar
Jean-Paul Smets committed
376
    # Stock Management
Romain Courteaud's avatar
Romain Courteaud committed
377 378 379
    ####################################################
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventory')
380
    def getInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
381
      """
382
      Returns inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
383
      """
384 385 386
      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
387

Romain Courteaud's avatar
Romain Courteaud committed
388 389
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventory')
390
    def getCurrentInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
391
      """
392
      Returns current inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
393
      """
394 395 396
      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
397

Romain Courteaud's avatar
Romain Courteaud committed
398 399
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getAvailableInventory')
400
    def getAvailableInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
401
      """
402 403
      Returns available inventory
      (current inventory - deliverable)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
404
      """
405 406 407
      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
408

Romain Courteaud's avatar
Romain Courteaud committed
409 410
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventory')
411
    def getFutureInventory(self, **kw):
412 413 414
      """
      Returns inventory at infinite
      """
415 416 417
      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
418

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

Romain Courteaud's avatar
Romain Courteaud committed
429 430
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryList')
431
    def getCurrentInventoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
432
      """
433
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
434
      """
435 436 437 438 439 440 441 442 443 444 445 446 447
      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
448

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

Romain Courteaud's avatar
Romain Courteaud committed
459 460
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryStat')
461
    def getInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
462
      """
463
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
464
      """
465 466 467
      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
468

Romain Courteaud's avatar
Romain Courteaud committed
469 470
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryStat')
471
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
472
      """
473
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
474
      """
475 476 477 478 479 480 481 482 483 484 485 486 487
      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
488

Romain Courteaud's avatar
Romain Courteaud committed
489 490
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryStat')
491
    def getFutureInventoryStat(self, **kw):
492 493 494
      """
      Returns statistics of inventory grouped by section or site
      """
495 496 497
      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
498

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

Romain Courteaud's avatar
Romain Courteaud committed
509 510
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getCurrentInventoryChart')
511
    def getCurrentInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
512
      """
513
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
514
      """
515 516 517
      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
518

Romain Courteaud's avatar
Romain Courteaud committed
519 520
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getFutureInventoryChart')
521
    def getFutureInventoryChart(self, **kw):
522 523 524
      """
      Returns list of inventory grouped by section or site
      """
525 526 527
      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
528

Romain Courteaud's avatar
Romain Courteaud committed
529 530
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryList')
531
    def getInventoryHistoryList(self, **kw):
532 533 534
      """
      Returns list of inventory grouped by section or site
      """
535 536 537
      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
538

Romain Courteaud's avatar
Romain Courteaud committed
539 540
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getInventoryHistoryChart')
541
    def getInventoryHistoryChart(self, **kw):
542 543 544
      """
      Returns list of inventory grouped by section or site
      """
545 546 547
      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
548

549 550 551 552
    # XXX FIXME
    # Method getCurrentMovementHistoryList, 
    # getAvailableMovementHistoryList, getFutureMovementHistoryList
    # can be added
Romain Courteaud's avatar
Romain Courteaud committed
553 554
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryList')
555
    def getMovementHistoryList(self, **kw):
556 557 558
      """
      Returns list of inventory grouped by section or site
      """
559 560 561
      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
562

Romain Courteaud's avatar
Romain Courteaud committed
563 564
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getMovementHistoryStat')
565
    def getMovementHistoryStat(self, **kw):
566 567 568
      """
      Returns list of inventory grouped by section or site
      """
569 570 571
      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
572

Romain Courteaud's avatar
Romain Courteaud committed
573 574
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getNextNegativeInventoryDate')
575
    def getNextNegativeInventoryDate(self, **kw):
576 577 578
      """
      Returns list of inventory grouped by section or site
      """
579 580 581
      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
582 583


584
    # Asset Price API
585 586
    security.declareProtected(Permissions.AccessContentsInformation,
        'getInventoryAssetPrice')
587
    def getInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
588
      """
589
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
590
      """
591 592 593
      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
594

595 596
    security.declareProtected(Permissions.AccessContentsInformation,
        'getCurrentInventoryAssetPrice')
597
    def getCurrentInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
598
      """
599
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
600
      """
601 602 603
      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
604

605 606
    security.declareProtected(Permissions.AccessContentsInformation,
        'getAvailableInventoryAssetPrice')
607
    def getAvailableInventoryAssetPrice(self, **kw):
608 609 610
      """
      Returns list of inventory grouped by section or site
      """
611 612 613
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getAvailableInventoryAssetPrice(**kw)
614

615 616
    security.declareProtected(Permissions.AccessContentsInformation,
        'getFutureInventoryAssetPrice')
617
    def getFutureInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
618
      """
619
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
620
      """
621 622 623
      kw['resource_uid'] = self.getUid()
      portal_simulation = getToolByName(self, 'portal_simulation')
      return portal_simulation.getFutureInventoryAssetPrice(**kw)
624

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

626
    # Industrial price API
627 628
    security.declareProtected(Permissions.AccessContentsInformation,
        'getIndustrialPrice')
629 630 631 632 633 634 635 636 637 638 639 640
    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

641
    # Predicate handling
642 643
    security.declareProtected(Permissions.AccessContentsInformation, 
                              'asPredicate')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
644 645 646 647 648 649 650
    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',))
651 652
      p.setMembershipCriterionCategoryList(('resource/%s'
          % self.getRelativeUrl(),))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
653
      return p
654

655
    def _pricingSortMethod(self, a, b):
656 657 658 659
      # Simple method : the one that defines a destination section wins
      if a.getDestinationSection():
        return -1 # a defines a destination section and wins
      return 1 # a defines no destination section and loses
660

661
    security.declareProtected(Permissions.AccessContentsInformation, 
662
                              'getPriceParameterDict')
663
    def getPriceParameterDict(self, context=None, REQUEST=None, **kw):
664
      """
665
      Get all pricing parameters from Predicate.
666
      """
667
      # Search all categories context
668
      new_category_list = []
669
      if context is not None:
670
        new_category_list += context.getCategoryList()
671 672
      #XXX This should be 'category_list' instead of 'categories' to respect
      # the naming convention. Must take care of side effects when fixing
673 674 675
      if kw.has_key('categories'):
        new_category_list.extend(kw['categories'])
        del kw['categories']
676 677
      resource_category = 'resource/' + self.getRelativeUrl()
      if not resource_category in new_category_list:
678 679 680
        new_category_list += (resource_category, )
      # Generate the predicate mapped value
      # to get some price values.
681
      domain_tool = getToolByName(self,'portal_domains')
682
      portal_type_list = self.getPortalSupplyPathTypeList()
683

Alexandre Boeglin's avatar
Alexandre Boeglin committed
684 685 686 687 688 689 690 691 692 693 694
      # 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)
695 696 697
      # Get price parameters
      price_parameter_dict = {
        'base_price': None,
698 699 700
        'additional_price': [],
        'surcharge_ratio': [],
        'discount_ratio': [],
701
        'exclusive_discount_ratio': None,
702 703
        'variable_additional_price': [],
        'non_discountable_additional_price': [],
704
        'priced_quantity': None,
705
        'base_unit_price': None,
706
      }
Alexandre Boeglin's avatar
Alexandre Boeglin committed
707 708 709 710
      if mapped_value is None:
        return price_parameter_dict
      for price_parameter_name in price_parameter_dict.keys():
        price_parameter_value = \
711 712 713
          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
714 715 716 717 718 719
          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':
720
                price_parameter_dict[price_parameter_name] = \
Alexandre Boeglin's avatar
Alexandre Boeglin committed
721 722 723 724
                    max(price_parameter_value)
              else:
                price_parameter_dict[price_parameter_name] = \
                    price_parameter_value[0]
725 726
      return price_parameter_dict
      
727
    security.declareProtected(Permissions.AccessContentsInformation,
728 729
        'getPricingVariable')
    def getPricingVariable(self, context=None):
730 731 732 733
      """
      Return the value of the property used to calculate variable pricing
      This basically calls a script like Product_getPricingVariable
      """
734 735 736
      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)
737
      method = None
738
      if context is not None:
739
        method = context._getTypeBasedMethod('getPricingVariable')
740
      if method is None or context is None:
741
        method = self._getTypeBasedMethod('getPricingVariable')
742

743 744 745 746
      if method is None:
        return 0.0
      return float(method())

747
    security.declareProtected(Permissions.AccessContentsInformation, 
748 749 750 751 752 753 754 755
                              '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.
756 757
      # Note that this is based on self (i.e. a resource) instead of context
      # (i.e. a movement).
758
      method = self._getTypeBasedMethod('getPriceCalculationOperandDict')
759
      if method is not None:
760
        return method(default=default, movement=context, REQUEST=REQUEST, **kw)
761

762 763 764 765 766 767 768 769 770
      # 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
771 772 773 774 775
      # 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.

776
      price_parameter_dict = self.getPriceParameterDict(
777 778 779
                                     context=context, REQUEST=REQUEST, **kw)
      # Calculate the unit price
      unit_base_price = None
780
      # Calculate
781 782 783 784 785
#     ((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))
786 787 788 789 790
      # Or, as (nearly) one single line :
#     ((bp + S(ap) + v * S(vap))
#       * (1 - m(1, M(S(dr), edr)))
#       + S(ndap))
#     * (1 + S(sr))
791 792 793
      # 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
794 795
      # depends on discrete variations, but also on a continuous property
      # of the object
796

797
      base_price = price_parameter_dict['base_price']
798
      if base_price in [None, '']:
799 800
        # XXX Compatibility
        # base_price must not be defined on resource
801 802
        base_price = self.getBasePrice()
      if base_price not in [None, '']:
803 804
        unit_base_price = base_price
        # Sum additional price
805
        for additional_price in price_parameter_dict['additional_price']:
806
          unit_base_price += additional_price
807
        # Sum variable additional price
808
        variable_value = self.getPricingVariable(context=context)
809 810
        for variable_additional_price in \
            price_parameter_dict['variable_additional_price']:
811
          unit_base_price += variable_additional_price * variable_value
812
        # Discount
813 814 815
        sum_discount_ratio = 0
        for discount_ratio in price_parameter_dict['discount_ratio']:
          sum_discount_ratio += discount_ratio
816 817 818
        exclusive_discount_ratio = \
            price_parameter_dict['exclusive_discount_ratio']
        d_ratio = 0
819
        d_ratio = max(d_ratio, sum_discount_ratio)
820 821 822 823 824
        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
825 826 827 828 829 830 831 832 833
        # 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
834 835 836
      # Divide by the priced quantity if not (None, 0)
      if unit_base_price is not None\
          and price_parameter_dict['priced_quantity']:
837
        priced_quantity = price_parameter_dict['priced_quantity']
838
        unit_base_price = unit_base_price / priced_quantity
839
      # Return result
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
      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
865 866 867 868 869 870

    security.declareProtected(Permissions.AccessContentsInformation, 
                              'getQuantityPrecision')
    def getQuantityPrecision(self):
      """Return the floating point precision of a quantity.
      """
871 872 873
      try:
        return int(round(- log(self.getBaseUnitQuantity(), 10),0))
      except TypeError:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
874
        return 0
875
      return 0
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891


    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
892
      quantity = self.getQuantityUnitDefinitionRatio(traverse(quantity_unit))
893 894 895 896
      if quantity_unit.split('/', 1)[0] != management_unit.split('/', 1)[0]:
        measure = self.getDefaultMeasure(quantity_unit)
        quantity /= measure.getConvertedQuantity(variation_list)
      else:
897
        quantity /= self.getQuantityUnitDefinitionRatio(traverse(management_unit))
898 899 900 901
      return quantity

    # Unit conversion
    security.declareProtected(Permissions.AccessContentsInformation, 'convertQuantity')
902 903
    def convertQuantity(self, quantity, from_unit, to_unit, variation_list=(),
      transformed_resource=None, transformed_variation_list=()):
904 905 906
      # 'variation_list' parameter may be deprecated:
      # cf Measure.getConvertedQuantity
      try:
907
        result = quantity * self._getConversionRatio(from_unit, variation_list)\
908 909 910 911 912 913 914
                        / 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))
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
        return None

      if transformed_resource is not None:
        variation_text = '\n'.join(variation_list)
        transformed_variation_text = '\n'.join(transformed_variation_list)
        transformed_uid = transformed_resource.getUid()

        query = self.zGetTransformedResourceConversionRatio(\
                    ui = self.getUid(),
                    variation_text = variation_text,
                    transformed_uid = transformed_uid,
                    transformed_variation_text=transformed_variation_text,
                  )
        if len(query) == 0:
          LOG('Resource.convertQuantity', WARNING,
              'could not get Transformation associated to %s -> %s'
              % (transformed_resource.getRelativeUrl(),
                self.getRelativeUrl()))
          return None
        result *= query[0].quantity

      return result
937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972

    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]

973 974 975 976 977 978 979 980 981 982 983 984 985 986
    def _getQuantityUnitDefinitionDict(self):
      """
      Returns a dictionary representing the Unit Definitions that hold
      for the current resource.
        Keys: quantity_unit categories uids.
        Values: tuple (unit_definition_uid, quantity)
          * unit_definition_uid can be None if the quantity_unit is defined
            as a standard_quantity_unit (no Unit Conversion Definition defines
            it, its definition comes from a Unit Conversion Group)
          * quantity is a float, an amount, expressed in the
            standard_quantity_unit for the base category of the quantity_unit.
            For example, if mass/g is the global standard quantity_unit, all
            definitions for mass/* will be expressed in grams.
      """
987 988
      global_definition_dict = self.\
          QuantityUnitConversionModule_getUniversalDefinitionDict()
989

990 991
      # _getUniversalDefinitionDict is a cached function. Copy the object to
      # avoid modifying it
992
      result = global_definition_dict.copy()
993
      for definition_group in self.objectValues(portal_type= \
994
          'Quantity Unit Conversion Group'):
995 996 997
        if definition_group.getValidationState() != "validated":
          continue

998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
        standard_quantity_unit_value = definition_group.getQuantityUnitValue()
        if standard_quantity_unit_value is None:
          continue

        uid = standard_quantity_unit_value.getUid()
        try:
          reference_ratio = global_definition_dict[uid][1]
        except KeyError:
          LOG("Resource", WARNING,
              "could not find a global Unit Definition for '%s' while " \
              "indexing local Definition Group '%s'" % \
                  (standard_quantity_unit_value.getRelativeUrl(),
                   definition_group.getRelativeUrl()))
1011 1012
          continue

1013
        for definition in definition_group.objectValues(portal_type= \
1014
            'Quantity Unit Conversion Definition'):
1015 1016 1017
          if definition.getValidationState() != "validated":
            continue

1018 1019 1020
          unit_uid = definition.getQuantityUnitUid()
          if unit_uid is None:
            continue
1021

1022 1023
          definition_ratio = definition.getConversionRatio()
          if not definition_ratio:
1024
            continue
1025

1026 1027
          result[unit_uid] = (definition.getUid(),
                              definition_ratio*reference_ratio)
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049

      return result

    security.declareProtected(Permissions.AccessContentsInformation,
                              'getQuantityUnitConversionDefinitionRowList')
    def getQuantityUnitConversionDefinitionRowList(self):
      """
      Returns a list rows to insert in the quantity_unit_conversion table.
      Used by z_catalog_quantity_unit_conversion_list.
      """
      # XXX If one wanted to add variation-specific Unit Conversion Definitions
      #  he could use an approach very similar to the one used for Measure.
      #  Just add a variation VARCHAR column in quantity_unit_conversion table
      #  (defaulting as "^"). The column would contain the REGEX describing the
      #  variation, exactly as Measure.
      #  Resource_zGetInventoryList would then need expansion to match the
      #  product variation vs the quantity_unit_conversion REGEX.

      uid = self.getUid()
      row_list = []
      for unit_uid, value in self._getQuantityUnitDefinitionDict().iteritems():
        definition_uid, quantity = value
1050 1051 1052 1053
        row_list.append(dict(uid=definition_uid,
                             resource_uid=uid,
                             quantity_unit_uid=unit_uid,
                             quantity=quantity))
1054 1055 1056

      return row_list

1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
    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 ()

1068 1069
      quantity_unit_definition_dict = self._getQuantityUnitDefinitionDict()

1070 1071 1072 1073 1074
      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:
1075
          metric_type_map[metric_type] = None
1076
        else:
1077
          metric_type_map[metric_type] = measure
1078 1079

      insert_list = []
1080 1081
      for measure in metric_type_map.itervalues():
        if measure is not None:
1082
          insert_list += measure.asCatalogRowList(quantity_unit_definition_dict)
1083

1084 1085 1086 1087 1088 1089 1090
      quantity_unit = quantity_unit_value.getCategoryRelativeUrl()
      if self.getDefaultMeasure(quantity_unit) is None:
          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.
1091
            quantity_unit_uid = quantity_unit_value.getUid()
1092 1093 1094 1095 1096 1097 1098 1099 1100
            try:
              quantity = quantity_unit_definition_dict[quantity_unit_uid][1]
            except KeyError:
              LOG("Resource", WARNING,
                  "could not find an Unit Definition for '%s' while " \
                  "indexing Resource '%s'" % \
                     (quantity_unit_value.getRelativeUrl(),
                      self.getRelativeUrl()))
              quantity = None
1101

1102 1103 1104 1105
            metric_type_uid = self.getPortalObject().portal_categories \
                                  .getCategoryUid(metric_type, 'metric_type')
            if quantity and metric_type_uid:
              uid = self.getUid()
1106 1107 1108
              insert_list.append(dict(uid=uid, resource_uid=uid, variation='^',
                                  metric_type_uid=metric_type_uid,
                                  quantity=float(quantity)))
1109 1110

      return insert_list
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127

    def getQuantityUnitDefinitionRatio(self, quantity_unit_value):
      """
      get the ratio used to define the quantity unit quantity_unit_value.
      If the Resource has a local Quantity Unit conversion Definition,
      return the ratio from that Definition.
      If not, fetch a Definition in the Global Module.
      """
      portal = self.getPortalObject()
      quantity_unit_uid = quantity_unit_value.getUid()

      deprecated_quantity = quantity_unit_value.getProperty('quantity')
      if deprecated_quantity is not None:
        warn('quantity field of quantity_unit categories is deprecated.' \
           ' Please use Quantity Unit Conversion Definitions instead and' \
           ' reset the value of this field.', DeprecationWarning)

1128
        return float(deprecated_quantity)
1129 1130 1131 1132 1133

      query = self.ResourceModule_zGetQuantityUnitDefinitionRatio(
                            quantity_unit_uid=quantity_unit_uid,
                            resource_uid=self.getUid())
      return query[0].quantity