Delivery.py 34.9 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4
# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Romain Courteaud's avatar
Romain Courteaud committed
6
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
7 8
#
# WARNING: This program as such is intended to be used by professional
9
# programmers who take the whole responsibility of assessing all potential
Jean-Paul Smets's avatar
Jean-Paul Smets committed
10 11
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
12
# guarantees and support are strongly adviced to contract a Free Software
Jean-Paul Smets's avatar
Jean-Paul Smets committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# 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 32
import zope.interface

Jean-Paul Smets's avatar
Jean-Paul Smets committed
33
from AccessControl import ClassSecurityInfo
34
from Products.ERP5Type import Permissions, PropertySheet, interfaces
35
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36
from Products.ERP5Type.XMLObject import XMLObject
37
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
38
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
39
from Products.ERP5.mixin.composition import CompositionMixin
40
from Products.ERP5.mixin.rule import SimulableMixin
41 42
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
from zLOG import LOG, PROBLEM
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43

44
class Delivery(XMLObject, ImmobilisationDelivery, SimulableMixin,
45
               CompositionMixin, AmountGeneratorMixin):
46 47 48 49
    """
        Each time delivery is modified, it MUST launch a reindexing of
        inventories which are related to the resources contained in the Delivery
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
50 51 52
    # CMF Type Definition
    meta_type = 'ERP5 Delivery'
    portal_type = 'Delivery'
53
    isDelivery = ConstantGetter('isDelivery', value=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
54 55 56

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

    # Default Properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.XMLObject
                      , PropertySheet.CategoryCore
                      , PropertySheet.DublinCore
                      , PropertySheet.Task
                      , PropertySheet.Arrow
                      , PropertySheet.Movement
                      , PropertySheet.Delivery
                      , PropertySheet.Reference
69
                      , PropertySheet.Price
Jean-Paul Smets's avatar
Jean-Paul Smets committed
70 71
                      )

72
    # Declarative interfaces
73 74 75
    zope.interface.implements(interfaces.IAmountGenerator,
                              interfaces.IDivergenceController,
                              interfaces.IMovementCollection)
76

Jean-Paul Smets's avatar
Jean-Paul Smets committed
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    security.declareProtected(Permissions.AccessContentsInformation, 'isAccountable')
    def isAccountable(self):
      """
        Returns 1 if this needs to be accounted
        Only account movements which are not associated to a delivery
        Whenever delivery is there, delivery has priority
      """
      return 1

    # Pricing methods
    def _getTotalPrice(self, context):
      return 2.0

    def _getDefaultTotalPrice(self, context):
      return 3.0

    def _getSourceTotalPrice(self, context):
      return 4.0

    def _getDestinationTotalPrice(self, context):
      return 5.0

Yoshinori Okuji's avatar
Yoshinori Okuji committed
99
    security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultTotalPrice')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    def getDefaultTotalPrice(self, context=None, REQUEST=None, **kw):
      """
      """
      return self._getDefaultTotalPrice(self.asContext(context=context, REQUEST=REQUEST, **kw))

    security.declareProtected(Permissions.AccessContentsInformation, 'getSourceTotalPrice')
    def getSourceTotalPrice(self, context=None, REQUEST=None, **kw):
      """
      """
      return self._getSourceTotalPrice(self.asContext(context=context, REQUEST=REQUEST, **kw))

    security.declareProtected(Permissions.AccessContentsInformation, 'getDestinationTotalPrice')
    def getDestinationTotalPrice(self, context=None, REQUEST=None, **kw):
      """
      """
      return self._getDestinationTotalPrice(self.asContext(context=context, REQUEST=REQUEST, **kw))

117 118
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getTotalPrice')
119
    def getTotalPrice(self, fast=0, src__=0, base_contribution=None, rounding=False, **kw):
120 121 122 123
      """ Returns the total price for this order
        if the `fast` argument is set to a true value, then it use
        SQLCatalog to compute the price, otherwise it sums the total
        price of objects one by one.
Sebastien Robin's avatar
Sebastien Robin committed
124 125 126

        So if the order is not in the catalog, getTotalPrice(fast=1)
        will return 0, this is not a bug.
127 128

        base_contribution must be a relative url of a category.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
129
      """
130 131
      result = None
      if not fast:
132 133
        kw.setdefault( 'portal_type',
                       self.getPortalDeliveryMovementTypeList())
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
        if base_contribution is None:
          result = sum([ line.getTotalPrice(fast=0) for line in
                         self.objectValues(**kw) ])
        else:
          # Find amounts from movements in the delivery.
          if isinstance(base_contribution, (tuple, list)):
            base_contribution_list = base_contribution
          else:
            base_contribution_list = (base_contribution,)
          base_contribution_value_list = []
          portal_categories = self.portal_categories
          for relative_url in base_contribution_list:
            base_contribution_value = portal_categories.getCategoryValue(relative_url)
            if base_contribution_value is not None:
              base_contribution_value_list.append(base_contribution_value)
          if not base_contribution_value_list:
            # We cannot find any amount so that the result is 0.
            result = 0
          else:
            matched_movement_list = [
                movement
                for movement in self.getMovementList()
                if set(movement.getBaseContributionValueList()).intersection(base_contribution_value_list)]
            if rounding:
              portal_roundings = self.portal_roundings
              matched_movement_list = [
                  portal_roundings.getRoundingProxy(movement)
                  for movement in matched_movement_list]
            result = sum([movement.getTotalPrice()
                          for movement in matched_movement_list])
164 165 166 167 168 169 170
      else:
        kw['explanation_uid'] = self.getUid()
        kw.update(self.portal_catalog.buildSQLQuery(**kw))
        if src__:
          return self.Delivery_zGetTotal(src__=1, **kw)
        aggregate = self.Delivery_zGetTotal(**kw)[0]
        result = aggregate.total_price or 0
171
      method = self._getTypeBasedMethod('convertTotalPrice')
172
      if method is not None:
173 174
        return method(result)
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
175

176 177 178 179 180 181 182 183 184 185
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getTotalNetPrice')
    def getTotalNetPrice(self, fast=0, src__=0, **kw):
      """
        Same as getTotalPrice, but including Tax and Discount.
      """
      total_price = self.getTotalPrice(fast=fast, src__=src__, **kw)
      kw['portal_type'] = self.getPortalTaxMovementTypeList()
      return total_price + self.getTotalPrice(fast=fast, src__=src__, **kw)

186
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
187
                              'getTotalQuantity')
188
    def getTotalQuantity(self, fast=0, src__=0, **kw):
189 190 191 192
      """ Returns the total quantity of this order.
        if the `fast` argument is set to a true value, then it use
        SQLCatalog to compute the quantity, otherwise it sums the total
        quantity of objects one by one.
Sebastien Robin's avatar
Sebastien Robin committed
193 194 195

        So if the order is not in the catalog, getTotalQuantity(fast=1)
        will return 0, this is not a bug.
196
      """
197
      if not fast :
Romain Courteaud's avatar
Romain Courteaud committed
198 199
        kw.setdefault('portal_type',
                      self.getPortalDeliveryMovementTypeList())
200 201
        return sum([ line.getTotalQuantity(fast=0) for line in
                        self.objectValues(**kw) ])
202
      kw['explanation_uid'] = self.getUid()
203 204
      kw.update(self.portal_catalog.buildSQLQuery(**kw))
      if src__:
205 206
        return self.Delivery_zGetTotal(src__=1, **kw)
      aggregate = self.Delivery_zGetTotal(**kw)[0]
207
      return aggregate.total_quantity or 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
208

Jérome Perrin's avatar
Jérome Perrin committed
209 210
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
211 212 213
    def getDeliveryUid(self):
      return self.getUid()

Jérome Perrin's avatar
Jérome Perrin committed
214 215
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryValue')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
216
    def getDeliveryValue(self):
217 218 219 220 221
      """
      Deprecated, we should use getRootDeliveryValue instead
      """
      return self

Jérome Perrin's avatar
Jérome Perrin committed
222 223
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')
224 225 226 227 228
    def getRootDeliveryValue(self):
      """
      This method returns the delivery, it is usefull to retrieve the delivery
      from a line or a cell
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
229 230
      return self

Jérome Perrin's avatar
Jérome Perrin committed
231 232
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')
233 234 235
    def getDelivery(self):
      return self.getRelativeUrl()

236
    security.declareProtected(Permissions.AccessContentsInformation,
237 238
                             '_getMovementList')
    def _getMovementList(self, portal_type=None, **kw):
239
      """
240
      Return a list of movements
241
      """
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
      movement_portal_type_set = set(
        self.getPortalObject().getPortalMovementTypeList())
      movement_list = self.objectValues(
        portal_type=movement_portal_type_set, **kw)
      if movement_list:

        if isinstance(portal_type, str):
          portal_type = set((portal_type,))
        elif isinstance(portal_type, (list, tuple)):
          portal_type = set(portal_type)

        # Browse lines recursively and collect leafs.
        stack = [iter(movement_list)]
        movement_list = []
        while stack:
          for sub_object in stack[-1]:
            content_list = sub_object.objectValues(
              portal_type=movement_portal_type_set, **kw)
            if sub_object.hasCellContent():
              cell_list = sub_object.getCellValueList()
              if len(cell_list) != len(content_list):
                content_list = set(content_list).difference(cell_list)
                if content_list:
                  stack.append(iter(content_list))
                  break
              else:
                movement_list.extend(x for x in content_list
                  if portal_type is None or x.getPortalType() in portal_type)
            elif content_list:
              stack.append(iter(content_list))
              break
            elif portal_type is None or \
                 sub_object.getPortalType() in portal_type:
              movement_list.append(sub_object)
          else:
            del stack[-1]
278

Jean-Paul Smets's avatar
Jean-Paul Smets committed
279
      return movement_list
280 281 282 283 284 285 286 287
    
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getMovementList')
    def getMovementList(self, portal_type=None, **kw):
      """
       Return a list of movements.
      """
      return self._getMovementList(portal_type=portal_type, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
288

Jérome Perrin's avatar
Jérome Perrin committed
289 290
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')
291 292 293 294 295
    def getSimulatedMovementList(self):
      """
        Return a list of simulated movements.
        This does not contain Container Line or Container Cell.
      """
Jérome Perrin's avatar
Jérome Perrin committed
296 297
      return self.getMovementList(portal_type=
                          self.getPortalSimulatedMovementTypeList())
298

Jérome Perrin's avatar
Jérome Perrin committed
299 300
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
301 302 303 304 305
    def getInvoiceMovementList(self):
      """
        Return a list of simulated movements.
        This does not contain Container Line or Container Cell.
      """
Jérome Perrin's avatar
Jérome Perrin committed
306 307
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
308

Jérome Perrin's avatar
Jérome Perrin committed
309 310
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
311 312 313 314 315 316
    def getContainerList(self):
      """
        Return a list of root containers.
        This does not contain sub-containers.
      """
      container_list = []
Jérome Perrin's avatar
Jérome Perrin committed
317 318
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
319 320 321
        container_list.append(m)
      return container_list

Jean-Paul Smets's avatar
Jean-Paul Smets committed
322 323
    #######################################################
    # Causality computation
324
    security.declareProtected(Permissions.AccessContentsInformation, 'isConvergent')
325
    def isConvergent(self,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
326 327 328
      """
        Returns 0 if the target is not met
      """
329
      return int(not self.isDivergent(**kw))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
330

331
    security.declareProtected(Permissions.AccessContentsInformation, 'isSimulated')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
332
    def isSimulated(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
333
      """
334
        Returns 1 if all non-null movements have a delivery counterpart
Jean-Paul Smets's avatar
Jean-Paul Smets committed
335 336
        in the simulation
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
337
      for m in self.getMovementList():
338 339
        if m.getQuantity() and not m.isSimulated():
          return 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
340
      return 1
341

342
    security.declareProtected(Permissions.AccessContentsInformation, 'isDivergent')
343
    def isDivergent(self, fast=0, **kw):
344 345 346 347
      """
        Returns 1 if the target is not met according to the current information
        After and edit, the isOutOfTarget will be checked. If it is 1,
        a message is emitted
Jean-Paul Smets's avatar
Jean-Paul Smets committed
348

349 350
        emit targetUnreachable !
      """
351 352
      ## Note that fast option was removed. Now, fast=1 is ignored.
      
353
      # Check if the total quantity equals the total of each simulation movement quantity
Sebastien Robin's avatar
Sebastien Robin committed
354 355
      for simulation_movement in self._getAllRelatedSimulationMovementList():
        if simulation_movement.isDivergent():
356
          return 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
357 358
      return 0

359
    security.declareProtected(Permissions.AccessContentsInformation, 'getDivergenceList')
360
    def getDivergenceList(self, **kw):
361 362 363 364
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
Sebastien Robin's avatar
Sebastien Robin committed
365 366
      for simulation_movement in self._getAllRelatedSimulationMovementList():
         divergence_list.extend(simulation_movement.getDivergenceList())
367 368
      return divergence_list

369
    @UnrestrictedMethod
Sebastien Robin's avatar
Sebastien Robin committed
370
    def updateCausalityState(self, solve_automatically=True, **kw):
371 372 373 374 375
      """
      This is often called as an activity, it will check if the
      deliver is convergent, and if so it will put the delivery
      in a solved state, if not convergent in a diverged state
      """
Sebastien Robin's avatar
Sebastien Robin committed
376 377 378 379
      isTransitionPossible = \
          self.getPortalObject().portal_workflow.isTransitionPossible
      if isTransitionPossible(self, 'diverge') and \
          isTransitionPossible(self, 'converge'):
380
        if self.isDivergent(**kw):
381
          if solve_automatically and \
Sebastien Robin's avatar
Sebastien Robin committed
382 383 384 385
              isTransitionPossible(self, 'solve_automatically'):
            self.solveAutomatically()
          else:
            self.diverge()
386 387
        else:
          self.converge()
388

389 390 391 392 393 394 395 396 397 398
    def updateSimulation(self, calculate=False, **kw):
      if calculate:
        path = self.getPath()
        self.activate(
          after_tag=('built:'+path, 'expand:'+path),
          after_path_and_method_id=(path, '_localBuild'),
          ).updateCausalityState()
      if kw:
        super(Delivery, self).updateSimulation(**kw)

399
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
400 401
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
      """
      this method will unlink and delete movements in movement_uid_list and
      rebuild a new Packing List with them.
      1/ change date in simulation, call TargetSolver and expand
      2/ detach simulation movements from to-be-deleted movements
      3/ delete movements
        XXX make sure that all detached movements are deleted at the same
        time, else the interaction workflow would reattach them to a delivery
        rule.
      4/ call builder
      """
      tag_list = []
      movement_list = [x for x in self.getMovementList() if x.getUid() in
          movement_uid_list]
      if not movement_list: return

      deferred_simulation_movement_list = []
      # defer simulation movements
Fabien Morin's avatar
Fabien Morin committed
420
      if start_date is not None or stop_date is not None:
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
        for movement in movement_list:
          start_date = start_date or movement.getStartDate()
          stop_date = stop_date or movement.getStopDate()
          for s_m in movement.getDeliveryRelatedValueList():
            if s_m.getStartDate() != start_date or \
                s_m.getStopDate() != stop_date:
              s_m.edit(start_date=start_date, stop_date=stop_date)
              deferred_simulation_movement_list.append(s_m)

      solver_tag = '%s_splitAndDefer_solver' % self.getRelativeUrl()
      expand_tag = '%s_splitAndDefer_expand' % self.getRelativeUrl()
      detach_tag = '%s_splitAndDefer_detach' % self.getRelativeUrl()
      build_tag = '%s_splitAndDefer_build' % self.getRelativeUrl()
      # call solver and expand on deferrd movements
      for movement in movement_list:
436 437
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
438
      tag_list.append(solver_tag)
439
      kw = {'after_tag': tag_list[:], 'tag': expand_tag}
440
      for s_m in deferred_simulation_movement_list:
441
        s_m.expand('deferred', activate_kw=kw)
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
      tag_list.append(expand_tag)

      detached_movement_url_list = []
      deleted_movement_uid_list = []
      #detach simulation movements
      for movement in movement_list:
        movement_url = movement.getRelativeUrl()
        movement_uid = getattr(movement,'uid',None)
        if movement_uid: deleted_movement_uid_list.append(movement_uid)
        for s_m in movement.getDeliveryRelatedValueList():
          delivery_list = \
              [x for x in s_m.getDeliveryList() if x != movement_url]
          s_m.activate(after_tag=tag_list[:], tag=detach_tag).setDeliveryList(
              delivery_list)
          detached_movement_url_list.append(s_m.getRelativeUrl())
      tag_list.append(detach_tag)

      #delete delivery movements
      # deleteContent uses the uid as a activity tag
      self.activate(after_tag=tag_list[:]).deleteContent([movement.getId() for
          movement in movement_list])
      tag_list.extend(deleted_movement_uid_list)

      # update causality state on self, after deletion
      self.activate(after_tag=tag_list[:],
          activity='SQLQueue').updateCausalityState()

      # call builder on detached movements
      builder = getattr(self.portal_deliveries, delivery_builder)
      builder.activate(after_tag=tag_list[:], tag=build_tag).build(
          movement_relative_url_list=detached_movement_url_list)


Jean-Paul Smets's avatar
Jean-Paul Smets committed
475 476 477 478 479 480
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
481
      self.recursiveReindexObject(*k, **kw)
482
      # do not reexpand simulation: this is a task for DSolver / TSolver
Jean-Paul Smets's avatar
Jean-Paul Smets committed
483 484 485 486 487

    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
      resource_dict = {}
Romain Courteaud's avatar
Romain Courteaud committed
488 489
      for m in self.contentValues(filter={
                      'portal_type': self.getPortalMovementTypeList()}):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
490 491 492 493 494
        r = m.getResource()
        if r is not None:
          resource_dict[r] = 1
      return resource_dict.keys()

495
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
496
                              'getInventory')
497 498 499 500 501 502
    def getInventory(self, **kw):
      """
      Returns inventory
      """
      kw['resource'] = self._getMovementResourceList()
      return self.portal_simulation.getInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
503

504
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
505
                              'getCurrentInventory')
506
    def getCurrentInventory(self, **kw):
507 508 509
      """
      Returns current inventory
      """
Romain Courteaud's avatar
Romain Courteaud committed
510
      kw['resource'] = self._getMovementResourceList()
511
      return self.portal_simulation.getCurrentInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
512

513
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
514
                              'getAvailableInventory')
515
    def getAvailableInventory(self, **kw):
516 517 518 519
      """
      Returns available inventory
      (current inventory - deliverable)
      """
Romain Courteaud's avatar
Romain Courteaud committed
520
      kw['resource'] = self._getMovementResourceList()
521 522
      return self.portal_simulation.getAvailableInventory(**kw)

523
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
524
                              'getFutureInventory')
525
    def getFutureInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
526
      """
527
      Returns inventory at infinite
Jean-Paul Smets's avatar
Jean-Paul Smets committed
528
      """
Romain Courteaud's avatar
Romain Courteaud committed
529
      kw['resource'] = self._getMovementResourceList()
530
      return self.portal_simulation.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
531

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

541
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
542
                              'getCurrentInventoryList')
543
    def getCurrentInventoryList(self, **kw):
544 545 546
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
547
      kw['resource'] = self._getMovementResourceList()
548
      return self.portal_simulation.getCurrentInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
549

550
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
551
                              'getFutureInventoryList')
552
    def getFutureInventoryList(self, **kw):
553 554 555
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
556
      kw['resource'] = self._getMovementResourceList()
557
      return self.portal_simulation.getFutureInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
558

559
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
560
                              'getInventoryStat')
561
    def getInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
562
      """
563
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
564
      """
Romain Courteaud's avatar
Romain Courteaud committed
565
      kw['resource'] = self._getMovementResourceList()
566
      return self.portal_simulation.getInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
567

568
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
569
                              'getCurrentInventoryStat')
570
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
571
      """
572
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
573
      """
Romain Courteaud's avatar
Romain Courteaud committed
574
      kw['resource'] = self._getMovementResourceList()
575
      return self.portal_simulation.getCurrentInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
576

577
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
578
                              'getFutureInventoryStat')
579
    def getFutureInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
580
      """
581
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
582
      """
Romain Courteaud's avatar
Romain Courteaud committed
583
      kw['resource'] = self._getMovementResourceList()
584
      return self.portal_simulation.getFutureInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
585

586
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
587
                              'getInventoryChart')
588 589 590 591
    def getInventoryChart(self, **kw):
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
592
      kw['resource'] = self._getMovementResourceList()
593 594
      return self.portal_simulation.getInventoryChart(**kw)

595
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
596
                              'getCurrentInventoryChart')
597
    def getCurrentInventoryChart(self, **kw):
598 599 600
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
601
      kw['resource'] = self._getMovementResourceList()
602
      return self.portal_simulation.getCurrentInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
603

604
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
605
                              'getFutureInventoryChart')
606
    def getFutureInventoryChart(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
607
      """
608
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
609
      """
Romain Courteaud's avatar
Romain Courteaud committed
610
      kw['resource'] = self._getMovementResourceList()
611
      return self.portal_simulation.getFutureInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
612

613
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
614
                              'getInventoryHistoryList')
615
    def getInventoryHistoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
616
      """
617
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
618
      """
Romain Courteaud's avatar
Romain Courteaud committed
619
      kw['resource'] = self._getMovementResourceList()
620
      return self.portal_simulation.getInventoryHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
621

622
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
623
                              'getInventoryHistoryChart')
624
    def getInventoryHistoryChart(self, **kw):
625 626 627
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
628
      kw['resource'] = self._getMovementResourceList()
629
      return self.portal_simulation.getInventoryHistoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
630

631
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
632
                              'getMovementHistoryList')
633
    def getMovementHistoryList(self, **kw):
634 635 636
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
637
      kw['resource'] = self._getMovementResourceList()
638
      return self.portal_simulation.getMovementHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
639

640
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
641
                              'getMovementHistoryStat')
642
    def getMovementHistoryStat(self, **kw):
643 644 645
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
646
      kw['resource'] = self._getMovementResourceList()
647
      return self.portal_simulation.getMovementHistoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
648

Romain Courteaud's avatar
Romain Courteaud committed
649 650 651 652




653 654 655 656 657 658 659 660 661
# JPS: We must still decide if getInventoryAssetPrice is part of the Delivery API

#     security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryAssetPrice')
#     def getInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getInventoryAssetPrice(**kw)
662
#
663 664 665 666 667 668 669
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
670
#
671 672 673 674 675 676 677
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
678
#
679 680 681 682 683 684 685 686
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

687 688
    ##########################################################################
    # Applied Rule stuff
689

690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
    security.declareProtected(Permissions.AccessContentsInformation,
                              'localBuild')
    def localBuild(self, activity_kw=()):
      """Activate builders for this delivery

      The generated activity will find all buildable business links for this
      delivery, and call related builders, which will select all simulation
      movements part of the same explanation(s) as the delivery.

      XXX: Consider moving it to SimulableMixin if it's useful for
           Subscription Items.
      """
      # XXX: Previous implementation waited for expand activities of related
      #      documents and even suggested to look at explanation tree,
      #      instead of causalities. Is it required ?
      kw = {'priority': 3}
      kw.update(activity_kw)
      after_tag = kw.pop('after_tag', None)
      if isinstance(after_tag, basestring):
        after_tag = [after_tag]
710
      else:
711 712 713
        after_tag = list(after_tag) if after_tag else []
      after_tag.append('expand:' + self.getPath())
      self.activate(after_tag=after_tag, **kw)._localBuild()
714

715 716 717
    def _localBuild(self):
      """Do an immediate local build for this delivery"""
      return self.asComposedDocument().build(explanation=self)
718

719 720 721
    def _createRootAppliedRule(self):
      portal = self.getPortalObject()
      # Only create RAR if we are not in a "too early" or "too late" state.
722 723 724
      state = self.getSimulationState()
      if (state != 'deleted' and
          state not in portal.getPortalDraftOrderStateList()):
725
        return super(Delivery, self)._createRootAppliedRule()
726 727

    security.declareProtected( Permissions.AccessContentsInformation,
728
                               'getRootCausalityValueList')
729 730 731 732 733 734
    def getRootCausalityValueList(self):
      """
        Returns the initial causality value for this movement.
        This method will look at the causality and check if the
        causality has already a causality
      """
735 736 737
      causality_value_list = self.getCausalityValueList()
      if causality_value_list:
        initial_list = []
738
        for causality in causality_value_list:
739 740
          # The causality may be something which has not this method
          # (e.g. item)
741 742 743 744 745 746 747 748 749
          try:
            getRootCausalityValueList = causality.getRootCausalityValueList
          except AttributeError:
            continue
          assert causality != self
          initial_list += [x for x in getRootCausalityValueList()
                             if x not in initial_list]
        return initial_list
      return [self]
750 751 752 753 754

    # XXX Temp hack, should be removed has soon as the structure of
    # the order/delivery builder will be reviewed. It might
    # be reviewed if we plan to configure movement groups in the zmi
    security.declareProtected( Permissions.ModifyPortalContent,
755
                               'setRootCausalityValueList')
756
    def setRootCausalityValueList(self,value):
757
      """
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
      This is a hack
      """
      pass

    security.declareProtected( Permissions.AccessContentsInformation,
                               'getParentExplanationValue')
    def getParentExplanationValue(self):
      """
        This method should be removed as soon as movement groups
        will be rewritten. It is a temp hack
      """
      return self

    # XXX Temp hack, should be removed has soon as the structure of
    # the order/delivery builder will be reviewed. It might
    # be reviewed if we plan to configure movement groups in the zmi
    security.declareProtected( Permissions.ModifyPortalContent,
                               'setParentExplanationValue')
    def setParentExplanationValue(self,value):
      """
      This is a hack
779 780 781
      """
      pass

782 783 784
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
785 786 787 788 789
      # XXX - quite a hack, since no way to know...
      #       propper implementation should use business path definition
      #       however, the real question is "is this really necessary"
      #       since the main purpose of this method is superceded
      #       by IDivergenceController
790

791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getRootSpecialiseValue')
    def getRootSpecialiseValue(self, portal_type_list):
      """Returns first specialise value matching portal type"""
      def findSpecialiseValue(context):
        if context.getPortalType() in portal_type_list:
          return context
        if getattr(context, 'getSpecialiseValueList', None) is not None:
          for specialise in context.getSpecialiseValueList():
            specialise_value = findSpecialiseValue(specialise)
            if specialise_value is not None:
              return specialise_value
        return None
      return findSpecialiseValue(self)

806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849
    security.declareProtected( Permissions.ModifyPortalContent,
                               'disconnectSimulationMovementList')
    def disconnectSimulationMovementList(self, movement_list=None):
      """Disconnects simulation movements from delivery's lines

      If movement_list is passed only those movements will be disconnected
      from simulation.

      If movements in movement_list do not belong to current
      delivery they are silently ignored.

      Returns list of disconnected Simulation Movements.

      Known issues and open questions:
       * how to protect disconnection from completed delivery?
       * what to do if movements from movement_list do not belong to delivery?
       * it is required to remove causality relation from delivery or delivery
         lines??
      """
      delivery_movement_value_list = self.getMovementList()
      if movement_list is not None:
        movement_value_list = [self.restrictedTraverse(movement) for movement
            in movement_list]
        # only those how are in this delivery
        movement_value_list = [movement_value for movement_value in
            movement_value_list if movement_value
            in delivery_movement_value_list]
      else:
        movement_value_list = delivery_movement_value_list

      disconnected_simulation_movement_list = []
      for movement_value in movement_value_list:
        # Note: Relies on fact that is invoked, when simulation movements are
        # indexed properly
        for simulation_movement in movement_value \
            .getDeliveryRelatedValueList(portal_type='Simulation Movement'):
          simulation_movement.edit(
            delivery = None,
            delivery_ratio = None
          )
          disconnected_simulation_movement_list.append(
              simulation_movement.getRelativeUrl())

      return disconnected_simulation_movement_list
Sebastien Robin's avatar
Sebastien Robin committed
850 851 852

    def _getAllRelatedSimulationMovementList(self, **kw):
      movement_uid_list = [x.getUid() for x in self.getMovementList()]
853 854 855 856
      return movement_uid_list and \
        self.getPortalObject().portal_catalog.unrestrictedSearchResults(
          portal_type='Simulation Movement',
          delivery_uid=movement_uid_list, **kw)
Sebastien Robin's avatar
Sebastien Robin committed
857 858 859 860 861 862 863

    def getDivergentTesterAndSimulationMovementList(self):
      """
      This method returns a list of (tester, simulation_movement) for each divergence.
      """
      divergent_tester_list = []
      for simulation_movement in self._getAllRelatedSimulationMovementList():
864
        simulation_movement = simulation_movement.getObject()
Sebastien Robin's avatar
Sebastien Robin committed
865 866 867 868 869
        rule = simulation_movement.getParentValue().getSpecialiseValue()
        for tester in rule._getDivergenceTesterList(exclude_quantity=False):
          if tester.explain(simulation_movement) not in (None, []):
            divergent_tester_list.append((tester, simulation_movement))
      return divergent_tester_list