Resource.py 47.3 KB
Newer Older
Nicolas Delaby's avatar
Nicolas Delaby committed
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4
# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
6
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# 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.
#
##############################################################################

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
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from Products.ERP5Type.XMLMatrix import XMLMatrix
38
from Products.ERP5Type.XMLObject import XMLObject
39
from Products.ERP5Type.Base import Base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40

41
from Products.ERP5Type.Utils import cartesianProduct
42
from Products.ERP5.mixin.variated import VariatedMixin
43
from Products.CMFCategory.Renderer import Renderer
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44

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

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

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

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

    # Declarative properties
61
    property_sheets = ( PropertySheet.DublinCore
Jean-Paul Smets's avatar
Jean-Paul Smets committed
62 63 64
                      , PropertySheet.Price
                      , PropertySheet.Resource
                      , PropertySheet.Reference
65
                      , PropertySheet.Comment
66
                      , PropertySheet.FlowCapacity
67
                      , PropertySheet.DefaultSupply
68
                      , PropertySheet.Aggregated
Jean-Paul Smets's avatar
Jean-Paul Smets committed
69 70 71 72 73 74 75
                      )

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

          resource.getVariationRangeCategoryItemList
            => [(display, value)]
Nicolas Delaby's avatar
Nicolas Delaby committed
86

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
      ## 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
108
        """
109
        result = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
110
        if base_category_list is ():
111 112 113
          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
114
          base_category_list = (base_category_list,)
115

116 117 118 119 120
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList(),
            sort_on=[('title','ascending')])
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
121
        other_base_category_dict = dict([(i,1) for i in base_category_list])
122
 
Nicolas Delaby's avatar
Nicolas Delaby committed
123
        if not omit_individual_variation:
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
          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]
140
        # Get category variation
141
        if other_base_category_list:
142 143
          result.extend(super(Resource, self).getVariationRangeCategoryItemList(
              base_category_list=other_base_category_list,
144
              base=base, display_base_category=display_base_category, **kw))
145
        # Return result
146
        return result
147

Jean-Paul Smets's avatar
Jean-Paul Smets committed
148 149
    security.declareProtected(Permissions.AccessContentsInformation,
                                           'getVariationCategoryItemList')
150
    def getVariationCategoryItemList(self, base_category_list=(), 
151
                                     omit_optional_variation=0,
152 153 154 155 156 157 158 159 160 161 162 163 164
                                     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, 
165
                         display_id='title' (default value title)
166
      """
167 168
      base_category_list = base_category_list or \
          self.getVariationBaseCategoryList()
Nicolas Delaby's avatar
Nicolas Delaby committed
169

170
      individual_bc_list = self.getIndividualVariationBaseCategoryList()
171 172 173 174 175 176 177 178 179
      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]
Nicolas Delaby's avatar
Nicolas Delaby committed
180 181


182
      result = super(Resource, self).getVariationCategoryItemList(
Nicolas Delaby's avatar
Nicolas Delaby committed
183 184
                            base_category_list=other_bc_list,
                            display_base_category=display_base_category,
185
                            display_id=display_id, base=base, **kw)
Nicolas Delaby's avatar
Nicolas Delaby committed
186

187
      if not omit_individual_variation:
188 189 190 191
        individual_variation_list = self.searchFolder(
            portal_type=self.getPortalVariationTypeList())
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
192

193 194
        for variation in individual_variation_list:
          for base_category in variation.getVariationBaseCategoryList():
195 196 197 198 199 200
            # 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):
201 202 203 204
              # XXX append object, relative_url ?
              # XXX now, call Renderer a lot of time.
              # Better implementation needed
              result.extend(Renderer(
205 206 207 208 209
                  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]))
210 211 212
      return result

    security.declareProtected(Permissions.AccessContentsInformation,
213
                              'getVariationCategoryList')
214
    def getVariationCategoryList(self, default=[], base_category_list=(),
215
                                 omit_individual_variation=1, **kw):
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
      """
        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(
245 246
                    base_category_list=base_category_list,
                    omit_individual_variation=omit_individual_variation,**kw)
247
      return [x[1] for x in vcil]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
248 249 250 251 252 253 254

# 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
255
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
256 257 258 259 260 261 262 263
        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
264
      except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
265 266 267 268 269 270 271 272 273 274
        return None


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


275 276
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDefaultTransformationValue')
277 278 279
    def getDefaultTransformationValue(self, context=None):
      """
      If context is None, returns the first available transformation that
280 281
      use self as a Resource. If there are several candidates, return the
      Transformation that has the latest version.
282 283 284 285 286

      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.
      """
287 288 289 290
      method = self._getTypeBasedMethod('getDefaultTransformationValue')
      if method is not None:
        return method(context)

291
      if context is None:
Nicolas Dumazet's avatar
Nicolas Dumazet committed
292 293 294 295 296 297
        transformation_list = self.portal_catalog(
            portal_type="Transformation",
            resource_relative_url=self.getRelativeUrl(),
            sort_on=[('version', 'descending')],
            limit=1
        )
298 299
        if len(transformation_list) > 0:
          return transformation_list[0].getObject()
300
        return None
301

302
      method = context._getTypeBasedMethod('getDefaultTransformationValue')
303 304 305
      if method is not None:
        return method(context)

306
      transformation_list = self.portal_domains.searchPredicateList(context,
307 308
                                portal_type="Transformation",
                                limit=1)
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325

      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
326
      transformation from a complex real-world transformation.
327
      """
328
      method = self._getTypeBasedMethod(\
329 330 331 332
                        'getDefaultConversionTransformationValue')
      if method is not None:
        return method()

333
      return self.getDefaultTransformationValue(context=None)
334 335


336 337 338 339 340 341 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
    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
367
    ####################################################
Jean-Paul Smets's avatar
Jean-Paul Smets committed
368
    # Stock Management
Romain Courteaud's avatar
Romain Courteaud committed
369
    ####################################################
Nicolas Delaby's avatar
Nicolas Delaby committed
370
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
371
                              'getInventory')
372
    def getInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
373
      """
374
      Returns inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
375
      """
376
      kw['resource_uid'] = self.getUid()
377
      portal_simulation = self.getPortalObject().portal_simulation
378
      return portal_simulation.getInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
379

Nicolas Delaby's avatar
Nicolas Delaby committed
380
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
381
                              'getCurrentInventory')
382
    def getCurrentInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
383
      """
384
      Returns current inventory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
385
      """
386
      kw['resource_uid'] = self.getUid()
387
      portal_simulation = self.getPortalObject().portal_simulation
388
      return portal_simulation.getCurrentInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
389

Nicolas Delaby's avatar
Nicolas Delaby committed
390
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
391
                              'getAvailableInventory')
392
    def getAvailableInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
393
      """
394 395
      Returns available inventory
      (current inventory - deliverable)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
396
      """
397
      kw['resource_uid'] = self.getUid()
398
      portal_simulation = self.getPortalObject().portal_simulation
399
      return portal_simulation.getAvailableInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
400

Nicolas Delaby's avatar
Nicolas Delaby committed
401
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
402
                              'getFutureInventory')
403
    def getFutureInventory(self, **kw):
404 405 406
      """
      Returns inventory at infinite
      """
407
      kw['resource_uid'] = self.getUid()
408
      portal_simulation = self.getPortalObject().portal_simulation
409
      return portal_simulation.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
410

Nicolas Delaby's avatar
Nicolas Delaby committed
411
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
412
                              'getInventoryList')
413 414 415 416
    def getInventoryList(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
417
      kw['resource_uid'] = self.getUid()
418
      portal_simulation = self.getPortalObject().portal_simulation
419
      return portal_simulation.getInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
420

Nicolas Delaby's avatar
Nicolas Delaby committed
421
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
422
                              'getCurrentInventoryList')
423
    def getCurrentInventoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
424
      """
425
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
426
      """
427
      kw['resource_uid'] = self.getUid()
428
      portal_simulation = self.getPortalObject().portal_simulation
429 430
      return portal_simulation.getCurrentInventoryList(**kw)

Nicolas Delaby's avatar
Nicolas Delaby committed
431
    security.declareProtected(Permissions.AccessContentsInformation,
432 433 434 435 436 437
                              'getAvailableInventoryList')
    def getAvailableInventoryList(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
      kw['resource_uid'] = self.getUid()
438
      portal_simulation = self.getPortalObject().portal_simulation
439
      return portal_simulation.getAvailableInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
440

Nicolas Delaby's avatar
Nicolas Delaby committed
441
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
442
                              'getFutureInventoryList')
443
    def getFutureInventoryList(self, **kw):
444 445 446
      """
      Returns list of inventory grouped by section or site
      """
447
      kw['resource_uid'] = self.getUid()
448
      portal_simulation = self.getPortalObject().portal_simulation
449
      return portal_simulation.getFutureInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
450

Nicolas Delaby's avatar
Nicolas Delaby committed
451
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
452
                              'getInventoryStat')
453
    def getInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
454
      """
455
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
456
      """
457
      kw['resource_uid'] = self.getUid()
458
      portal_simulation = self.getPortalObject().portal_simulation
459
      return portal_simulation.getInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
460

Nicolas Delaby's avatar
Nicolas Delaby committed
461
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
462
                              'getCurrentInventoryStat')
463
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
464
      """
465
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
466
      """
467
      kw['resource_uid'] = self.getUid()
468
      portal_simulation = self.getPortalObject().portal_simulation
469 470 471 472 473 474 475 476 477
      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()
478
      portal_simulation = self.getPortalObject().portal_simulation
479
      return portal_simulation.getAvailableInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
480

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

Nicolas Delaby's avatar
Nicolas Delaby committed
491
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
492
                              'getInventoryChart')
493
    def getInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
494
      """
495
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
496
      """
497
      kw['resource_uid'] = self.getUid()
498
      portal_simulation = self.getPortalObject().portal_simulation
499
      return portal_simulation.getInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
500

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

Nicolas Delaby's avatar
Nicolas Delaby committed
511
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
512
                              'getFutureInventoryChart')
513
    def getFutureInventoryChart(self, **kw):
514 515 516
      """
      Returns list of inventory grouped by section or site
      """
517
      kw['resource_uid'] = self.getUid()
518
      portal_simulation = self.getPortalObject().portal_simulation
519
      return portal_simulation.getFutureInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
520

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

Nicolas Delaby's avatar
Nicolas Delaby committed
531
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
532
                              'getInventoryHistoryChart')
533
    def getInventoryHistoryChart(self, **kw):
534 535 536
      """
      Returns list of inventory grouped by section or site
      """
537
      kw['resource_uid'] = self.getUid()
538
      portal_simulation = self.getPortalObject().portal_simulation
539
      return portal_simulation.getInventoryHistoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
540

541 542 543 544
    # XXX FIXME
    # Method getCurrentMovementHistoryList, 
    # getAvailableMovementHistoryList, getFutureMovementHistoryList
    # can be added
Nicolas Delaby's avatar
Nicolas Delaby committed
545
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
546
                              'getMovementHistoryList')
547
    def getMovementHistoryList(self, **kw):
548 549 550
      """
      Returns list of inventory grouped by section or site
      """
551
      kw['resource_uid'] = self.getUid()
552
      portal_simulation = self.getPortalObject().portal_simulation
553
      return portal_simulation.getMovementHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
554

Nicolas Delaby's avatar
Nicolas Delaby committed
555
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
556
                              'getMovementHistoryStat')
557
    def getMovementHistoryStat(self, **kw):
558 559 560
      """
      Returns list of inventory grouped by section or site
      """
561
      kw['resource_uid'] = self.getUid()
562
      portal_simulation = self.getPortalObject().portal_simulation
563
      return portal_simulation.getMovementHistoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
564

Nicolas Delaby's avatar
Nicolas Delaby committed
565
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
566
                              'getNextNegativeInventoryDate')
567
    def getNextNegativeInventoryDate(self, **kw):
568 569 570
      """
      Returns list of inventory grouped by section or site
      """
571
      kw['resource_uid'] = self.getUid()
572
      portal_simulation = self.getPortalObject().portal_simulation
573
      return portal_simulation.getNextNegativeInventoryDate(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
574 575


576
    # Asset Price API
577 578
    security.declareProtected(Permissions.AccessContentsInformation,
        'getInventoryAssetPrice')
579
    def getInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
580
      """
581
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
582
      """
583
      kw['resource_uid'] = self.getUid()
584
      portal_simulation = self.getPortalObject().portal_simulation
585
      return portal_simulation.getInventoryAssetPrice(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
586

587 588
    security.declareProtected(Permissions.AccessContentsInformation,
        'getCurrentInventoryAssetPrice')
589
    def getCurrentInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
590
      """
591
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
592
      """
593
      kw['resource_uid'] = self.getUid()
594
      portal_simulation = self.getPortalObject().portal_simulation
595
      return portal_simulation.getCurrentInventoryAssetPrice(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
596

597 598
    security.declareProtected(Permissions.AccessContentsInformation,
        'getAvailableInventoryAssetPrice')
599
    def getAvailableInventoryAssetPrice(self, **kw):
600 601 602
      """
      Returns list of inventory grouped by section or site
      """
603
      kw['resource_uid'] = self.getUid()
604
      portal_simulation = self.getPortalObject().portal_simulation
605
      return portal_simulation.getAvailableInventoryAssetPrice(**kw)
606

607 608
    security.declareProtected(Permissions.AccessContentsInformation,
        'getFutureInventoryAssetPrice')
609
    def getFutureInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
610
      """
611
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
612
      """
613
      kw['resource_uid'] = self.getUid()
614
      portal_simulation = self.getPortalObject().portal_simulation
615
      return portal_simulation.getFutureInventoryAssetPrice(**kw)
616

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

618
    # Industrial price API
619 620
    security.declareProtected(Permissions.AccessContentsInformation,
        'getIndustrialPrice')
621 622 623 624 625 626 627 628 629 630 631 632
    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

633
    # Predicate handling
Nicolas Delaby's avatar
Nicolas Delaby committed
634
    security.declareProtected(Permissions.AccessContentsInformation,
635
                              'asPredicate')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
636 637 638 639 640 641 642
    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',))
643 644
      p.setMembershipCriterionCategoryList(('resource/%s'
          % self.getRelativeUrl(),))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
645
      return p
646

647
    def _pricingSortMethod(self, a, b):
648 649 650 651
      # 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
652

Nicolas Delaby's avatar
Nicolas Delaby committed
653
    security.declareProtected(Permissions.AccessContentsInformation,
654
                              'getPriceParameterDict')
655 656
    def getPriceParameterDict(self, context=None, REQUEST=None,
                              supply_path_type=None, **kw):
657
      """
658
      Get all pricing parameters from Predicate.
659
      """
660
      # Search all categories context
661 662 663 664
      if context is None:
        new_category_list = []
      else:
        new_category_list = context.getCategoryList()
665 666
      #XXX This should be 'category_list' instead of 'categories' to respect
      # the naming convention. Must take care of side effects when fixing
667
      new_category_list += kw.pop('categories', ())
668 669
      resource_category = 'resource/' + self.getRelativeUrl()
      if not resource_category in new_category_list:
670
        new_category_list.append(resource_category)
671 672
      # Generate the predicate mapped value
      # to get some price values.
673
      portal = self.getPortalObject()
674
      if supply_path_type is None:
675 676
        portal_type_list = kw.pop('portal_type',
                                  portal.getPortalSupplyPathTypeList())
677 678 679 680
      elif isinstance(supply_path_type, (list, tuple)):
        portal_type_list = supply_path_type
      else:
        portal_type_list = (supply_path_type,)
681

682
      sort_method = kw.pop('sort_method', self._pricingSortMethod)
Alexandre Boeglin's avatar
Alexandre Boeglin committed
683
      # Generate the fake context
684 685 686
      tmp_context = self.asContext(context=context,
                                   categories=new_category_list,
                                   REQUEST=REQUEST, **kw)
687
      # XXX When called for a generated amount, base_application may point
688
      #     to nonexistant base_amount (e.g. "base_amount/produced_quantity" for
689 690
      #     transformations), which would make domain tool return nothing.
      #     Following hack cleans up a category we don't want to test anyway.
691 692 693
      #     Also, do not use '_setBaseApplication' to bypass interactions.
      portal.portal_categories._setCategoryMembership(tmp_context,
        ('base_application',), ())
694
      mapped_value = portal.portal_domains.generateMultivaluedMappedValue(
Alexandre Boeglin's avatar
Alexandre Boeglin committed
695 696
                                             tmp_context,
                                             portal_type=portal_type_list,
697 698
                                             has_cell_content=0,
                                             sort_method=sort_method, **kw)
699 700 701
      # Get price parameters
      price_parameter_dict = {
        'base_price': None,
702 703 704
        'additional_price': [],
        'surcharge_ratio': [],
        'discount_ratio': [],
705
        'exclusive_discount_ratio': None,
706 707
        'variable_additional_price': [],
        'non_discountable_additional_price': [],
708
        'priced_quantity': None,
709
        'base_unit_price': None,
710
      }
Alexandre Boeglin's avatar
Alexandre Boeglin committed
711 712 713 714
      if mapped_value is None:
        return price_parameter_dict
      for price_parameter_name in price_parameter_dict.keys():
        price_parameter_value = \
715 716 717
          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
718 719 720 721 722 723
          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':
724
                price_parameter_dict[price_parameter_name] = \
Alexandre Boeglin's avatar
Alexandre Boeglin committed
725 726 727 728
                    max(price_parameter_value)
              else:
                price_parameter_dict[price_parameter_name] = \
                    price_parameter_value[0]
729
      return price_parameter_dict
Nicolas Delaby's avatar
Nicolas Delaby committed
730

731
    security.declareProtected(Permissions.AccessContentsInformation,
732 733
        'getPricingVariable')
    def getPricingVariable(self, context=None):
734 735 736 737
      """
      Return the value of the property used to calculate variable pricing
      This basically calls a script like Product_getPricingVariable
      """
738 739 740
      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)
741
      method = None
742
      if context is not None:
743
        method = context._getTypeBasedMethod('getPricingVariable')
744
      if method is None or context is None:
745
        method = self._getTypeBasedMethod('getPricingVariable')
746

747 748 749 750
      if method is None:
        return 0.0
      return float(method())

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

766 767 768 769 770 771 772 773 774
      # 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
775 776 777 778 779
      # 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.

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

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

Nicolas Delaby's avatar
Nicolas Delaby committed
848
    security.declareProtected(Permissions.AccessContentsInformation,
849 850 851 852 853 854 855 856 857 858 859 860 861 862
                              '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
Nicolas Delaby's avatar
Nicolas Delaby committed
863 864

      operand_dict = self.getPriceCalculationOperandDict(default=default,
865 866 867 868
              context=context, REQUEST=REQUEST, **kw)
      if operand_dict is not None:
        return operand_dict['price']
      return default
Yoshinori Okuji's avatar
Yoshinori Okuji committed
869

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


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

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

    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]

977 978 979 980 981 982 983 984 985 986 987 988 989 990
    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.
      """
991 992
      global_definition_dict = self.\
          QuantityUnitConversionModule_getUniversalDefinitionDict()
993

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

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014
        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()))
1015 1016
          continue

1017
        for definition in definition_group.objectValues(portal_type= \
1018
            'Quantity Unit Conversion Definition'):
1019 1020 1021
          if definition.getValidationState() != "validated":
            continue

1022 1023 1024
          unit_uid = definition.getQuantityUnitUid()
          if unit_uid is None:
            continue
1025

1026 1027
          definition_ratio = definition.getConversionRatio()
          if not definition_ratio:
1028
            continue
1029

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

      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
1054 1055 1056 1057
        row_list.append(dict(uid=definition_uid,
                             resource_uid=uid,
                             quantity_unit_uid=unit_uid,
                             quantity=quantity))
1058 1059 1060

      return row_list

1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
    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 ()

1072 1073
      quantity_unit_definition_dict = self._getQuantityUnitDefinitionDict()

1074 1075 1076 1077 1078
      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:
1079
          metric_type_map[metric_type] = None
1080
        else:
1081
          metric_type_map[metric_type] = measure
1082 1083

      insert_list = []
1084 1085
      for measure in metric_type_map.itervalues():
        if measure is not None:
1086
          insert_list += measure.asCatalogRowList(quantity_unit_definition_dict)
1087

1088 1089 1090 1091 1092 1093 1094
      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.
1095
            quantity_unit_uid = quantity_unit_value.getUid()
1096 1097 1098 1099 1100 1101 1102 1103 1104
            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
1105

1106 1107 1108 1109
            metric_type_uid = self.getPortalObject().portal_categories \
                                  .getCategoryUid(metric_type, 'metric_type')
            if quantity and metric_type_uid:
              uid = self.getUid()
1110 1111 1112
              insert_list.append(dict(uid=uid, resource_uid=uid, variation='^',
                                  metric_type_uid=metric_type_uid,
                                  quantity=float(quantity)))
1113 1114

      return insert_list
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131

    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)

1132
        return float(deprecated_quantity)
1133 1134 1135 1136 1137

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