Resource.py 43 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
40
from Products.ERP5Type.UnrestrictedMethod import unrestricted_apply
Jean-Paul Smets's avatar
Jean-Paul Smets committed
41

42
from Products.ERP5Type.Utils import cartesianProduct
43
from Products.ERP5.mixin.variated import VariatedMixin
44
from Products.CMFCategory.Renderer import Renderer
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(XMLObject, XMLMatrix, VariatedMixin):
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 properties
62
    property_sheets = ( PropertySheet.DublinCore
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63 64 65
                      , PropertySheet.Price
                      , PropertySheet.Resource
                      , PropertySheet.Reference
66
                      , PropertySheet.Comment
67
                      , PropertySheet.FlowCapacity
68
                      , PropertySheet.DefaultSupply
69
                      , PropertySheet.Aggregated
Jean-Paul Smets's avatar
Jean-Paul Smets committed
70 71 72 73 74 75 76
                      )

    # 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
77 78
    def getVariationRangeCategoryItemList(self, base_category_list=(), base=1,
                                          root=1, display_id='title',
79
                                          display_base_category=1,
80 81
                                          current_category=None,
                                          omit_individual_variation=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82 83
        """
          Returns possible variations
84 85 86

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

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

117
        individual_variation_list = self.contentValues(
118 119 120 121
            portal_type=self.getPortalVariationTypeList(),
            sort_on=[('title','ascending')])
        individual_variation_list = [x.getObject() for x in
            individual_variation_list]
122
        other_base_category_set = set(base_category_list)
123

Nicolas Delaby's avatar
Nicolas Delaby committed
124
        if not omit_individual_variation:
125 126 127 128
          for variation in individual_variation_list:
            for base_category in variation.getVariationBaseCategoryList():
              if base_category_list is ()\
                  or base_category in base_category_list:
129
                other_base_category_set.discard(base_category)
130 131 132
                # XXX now, call Renderer a lot of time.
                # Better implementation needed
                result.extend(Renderer(
133
                    base_category=base_category,
134 135 136 137 138
                    display_base_category=display_base_category,
                    display_none_category=0, base=base,
                    current_category=current_category,
                    display_id=display_id).render([variation]))

139
        # Get category variation
140
        if other_base_category_set:
141
          result.extend(super(Resource, self).getVariationRangeCategoryItemList(
142
              base_category_list=list(other_base_category_set),
143
              base=base, display_base_category=display_base_category, **kw))
144
        # Return result
145
        return result
146

Jean-Paul Smets's avatar
Jean-Paul Smets committed
147 148
    security.declareProtected(Permissions.AccessContentsInformation,
                                           'getVariationCategoryItemList')
149
    def getVariationCategoryItemList(self, base_category_list=(),
150
                                     omit_optional_variation=0,
151 152 153 154 155 156
                                     omit_individual_variation=1, base=1,
                                     current_category=None,
                                     display_base_category=1,
                                     display_id='title', **kw):
      """
        Returns variations of the resource.
157
        If omit_individual_variation==1, does not return individual
158 159 160 161 162
        variation.
        Else, returns them.
        Display is on left.
            => [(display, value)]

163
        *old parameters: base=1, current_category=None,
164
                         display_id='title' (default value title)
165
      """
166 167
      base_category_list = base_category_list or \
          self.getVariationBaseCategoryList()
Nicolas Delaby's avatar
Nicolas Delaby committed
168

169
      individual_bc_list = self.getIndividualVariationBaseCategoryList()
170 171 172 173 174 175 176 177 178
      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
179 180


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

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

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

    security.declareProtected(Permissions.AccessContentsInformation,
212
                              'getVariationCategoryList')
213
    def getVariationCategoryList(self, default=[], base_category_list=(),
214
                                 omit_individual_variation=1, **kw):
215 216
      """
        Returns variations of the resource.
217
        If omit_individual_variation==1, does not return individual
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
        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(
244 245
                    base_category_list=base_category_list,
                    omit_individual_variation=omit_individual_variation,**kw)
246
      return [x[1] for x in vcil]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
247 248 249 250 251 252 253

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Nicolas Delaby's avatar
Nicolas Delaby committed
564
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
565
                              'getNextNegativeInventoryDate')
566
    def getNextNegativeInventoryDate(self, **kw):
567
      """
568 569 570 571 572 573
      Returns next date where the inventory will be negative
      """
      return self.getNextAlertInventoryDate(
                  reference_quantity=0, **kw)

    security.declareProtected(Permissions.AccessContentsInformation,
574
                              'getNextAlertInventoryDate')
575 576 577 578
    def getNextAlertInventoryDate(self, reference_quantity=0, **kw):
      """
      Returns next date where the inventory will be below reference
      quantity
579
      """
580
      kw['resource_uid'] = self.getUid()
581
      portal_simulation = self.getPortalObject().portal_simulation
582 583
      return portal_simulation.getNextAlertInventoryDate(
                          reference_quantity=reference_quantity, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
584

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

596 597
    security.declareProtected(Permissions.AccessContentsInformation,
        'getCurrentInventoryAssetPrice')
598
    def getCurrentInventoryAssetPrice(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
599
      """
600
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
601
      """
602
      kw['resource_uid'] = self.getUid()
603
      portal_simulation = self.getPortalObject().portal_simulation
604
      return portal_simulation.getCurrentInventoryAssetPrice(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
605

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

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

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

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

642
    def _pricingSortKeyMethod(self, a):
643 644 645 646
      # 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
647

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

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

Nicolas Delaby's avatar
Nicolas Delaby committed
730
    security.declareProtected(Permissions.AccessContentsInformation,
731 732 733 734 735 736 737
                              '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.
      """
738 739 740
      kw.update(default=default, movement=context, REQUEST=REQUEST)
      return unrestricted_apply(
        self._getTypeBasedMethod('getPriceCalculationOperandDict'), kw=kw)
741

Nicolas Delaby's avatar
Nicolas Delaby committed
742
    security.declareProtected(Permissions.AccessContentsInformation,
743 744 745 746 747 748 749 750 751 752 753 754 755 756
                              '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
757 758

      operand_dict = self.getPriceCalculationOperandDict(default=default,
759 760 761 762
              context=context, REQUEST=REQUEST, **kw)
      if operand_dict is not None:
        return operand_dict['price']
      return default
Yoshinori Okuji's avatar
Yoshinori Okuji committed
763

Nicolas Delaby's avatar
Nicolas Delaby committed
764
    security.declareProtected(Permissions.AccessContentsInformation,
Yoshinori Okuji's avatar
Yoshinori Okuji committed
765 766 767 768
                              'getQuantityPrecision')
    def getQuantityPrecision(self):
      """Return the floating point precision of a quantity.
      """
769 770 771
      try:
        return int(round(- log(self.getBaseUnitQuantity(), 10),0))
      except TypeError:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
772
        return 0
773
      return 0
774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789


    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
790
      quantity = self.getQuantityUnitDefinitionRatio(traverse(quantity_unit))
791 792 793 794
      if quantity_unit.split('/', 1)[0] != management_unit.split('/', 1)[0]:
        measure = self.getDefaultMeasure(quantity_unit)
        quantity /= measure.getConvertedQuantity(variation_list)
      else:
795
        quantity /= self.getQuantityUnitDefinitionRatio(traverse(management_unit))
796 797 798 799
      return quantity

    # Unit conversion
    security.declareProtected(Permissions.AccessContentsInformation, 'convertQuantity')
800 801
    def convertQuantity(self, quantity, from_unit, to_unit, variation_list=(),
      transformed_resource=None, transformed_variation_list=()):
802 803 804
      # 'variation_list' parameter may be deprecated:
      # cf Measure.getConvertedQuantity
      try:
805
        result = quantity * self._getConversionRatio(from_unit, variation_list)\
806 807 808 809 810 811 812
                        / 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))
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834
        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
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870

    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]

871 872 873 874 875 876 877 878 879 880 881 882 883 884
    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.
      """
885 886
      global_definition_dict = self.\
          QuantityUnitConversionModule_getUniversalDefinitionDict()
887

888 889
      # _getUniversalDefinitionDict is a cached function. Copy the object to
      # avoid modifying it
890
      result = global_definition_dict.copy()
891
      for definition_group in self.objectValues(portal_type= \
892
          'Quantity Unit Conversion Group'):
893 894 895
        if definition_group.getValidationState() != "validated":
          continue

896 897 898 899 900 901 902 903 904 905 906 907 908
        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()))
909 910
          continue

911
        for definition in definition_group.objectValues(portal_type= \
912
            'Quantity Unit Conversion Definition'):
913 914 915
          if definition.getValidationState() != "validated":
            continue

916 917 918
          unit_uid = definition.getQuantityUnitUid()
          if unit_uid is None:
            continue
919

920 921
          definition_ratio = definition.getConversionRatio()
          if not definition_ratio:
922
            continue
923

924 925
          result[unit_uid] = (definition.getUid(),
                              definition_ratio*reference_ratio)
926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947

      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
948 949 950 951
        row_list.append(dict(uid=definition_uid,
                             resource_uid=uid,
                             quantity_unit_uid=unit_uid,
                             quantity=quantity))
952 953 954

      return row_list

955 956 957 958 959 960 961 962 963 964 965
    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 ()

966 967
      quantity_unit_definition_dict = self._getQuantityUnitDefinitionDict()

968 969 970 971 972
      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:
973
          metric_type_map[metric_type] = None
974
        else:
975
          metric_type_map[metric_type] = measure
976 977

      insert_list = []
978 979
      for measure in metric_type_map.itervalues():
        if measure is not None:
980
          insert_list += measure.asCatalogRowList(quantity_unit_definition_dict)
981

982 983 984 985 986 987 988
      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.
989
            quantity_unit_uid = quantity_unit_value.getUid()
990 991 992 993 994 995 996 997 998
            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
999

1000 1001 1002 1003
            metric_type_uid = self.getPortalObject().portal_categories \
                                  .getCategoryUid(metric_type, 'metric_type')
            if quantity and metric_type_uid:
              uid = self.getUid()
1004 1005 1006
              insert_list.append(dict(uid=uid, resource_uid=uid, variation='^',
                                  metric_type_uid=metric_type_uid,
                                  quantity=float(quantity)))
1007 1008

      return insert_list
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025

    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)

1026
        return float(deprecated_quantity)
1027 1028 1029 1030

      query = self.ResourceModule_zGetQuantityUnitDefinitionRatio(
                            quantity_unit_uid=quantity_unit_uid,
                            resource_uid=self.getUid())
1031 1032
      try:
        return query[0].quantity
1033 1034 1035 1036 1037 1038
      except IndexError:
        raise IndexError('Can not find the Quantity Unit Conversion '\
                         'Definition. Please make sure that Unit '\
                         'Conversion Definitions are indexed and validated. '\
                         'quantity_unit_uid: %s, resource_uid: %s' \
                          % (quantity_unit_uid, self.getUid()))