Delivery.py 35.2 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 35 36
from AccessControl.SecurityManagement import getSecurityManager, \
    setSecurityManager, newSecurityManager
from AccessControl.User import nobody
37
from Products.ERP5Type import Permissions, PropertySheet, interfaces
38
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39
from Products.ERP5Type.XMLObject import XMLObject
40
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
41
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
42
from Products.ERP5.mixin.composition import CompositionMixin
43
from Products.ERP5.mixin.rule import SimulableMixin
44 45
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod, \
    unrestricted_apply
46
from zLOG import LOG, PROBLEM
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47

48
class Delivery(XMLObject, ImmobilisationDelivery, SimulableMixin,
49
               CompositionMixin, AmountGeneratorMixin):
50 51 52 53
    """
        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
54 55 56
    # CMF Type Definition
    meta_type = 'ERP5 Delivery'
    portal_type = 'Delivery'
57
    isDelivery = ConstantGetter('isDelivery', value=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
58 59 60

    # Declarative security
    security = ClassSecurityInfo()
61
    security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
62 63 64 65 66 67 68 69 70 71 72

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

76
    # Declarative interfaces
77 78 79
    zope.interface.implements(interfaces.IAmountGenerator,
                              interfaces.IDivergenceController,
                              interfaces.IMovementCollection)
80

Jean-Paul Smets's avatar
Jean-Paul Smets committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    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
103
    security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultTotalPrice')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    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))

121 122
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getTotalPrice')
123
    def getTotalPrice(self, fast=0, src__=0, base_contribution=None, rounding=False, **kw):
124 125 126 127
      """ 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
128 129 130

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

        base_contribution must be a relative url of a category.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
133
      """
134 135
      result = None
      if not fast:
136 137
        kw.setdefault( 'portal_type',
                       self.getPortalDeliveryMovementTypeList())
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 164 165 166 167
        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])
168 169 170 171 172 173 174
      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
175
      method = self._getTypeBasedMethod('convertTotalPrice')
176
      if method is not None:
177 178
        return method(result)
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
179

180 181 182 183 184 185 186 187 188 189
    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)

190
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
191
                              'getTotalQuantity')
192
    def getTotalQuantity(self, fast=0, src__=0, **kw):
193 194 195 196
      """ 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
197 198 199

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

Jérome Perrin's avatar
Jérome Perrin committed
213 214
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
215 216 217
    def getDeliveryUid(self):
      return self.getUid()

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

Jérome Perrin's avatar
Jérome Perrin committed
226 227
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')
228 229 230 231 232
    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
233 234
      return self

Jérome Perrin's avatar
Jérome Perrin committed
235 236
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')
237 238 239
    def getDelivery(self):
      return self.getRelativeUrl()

240
    security.declareProtected(Permissions.AccessContentsInformation,
241 242
                             '_getMovementList')
    def _getMovementList(self, portal_type=None, **kw):
243
      """
244
      Return a list of movements
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 278 279 280 281
      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]
282

Jean-Paul Smets's avatar
Jean-Paul Smets committed
283
      return movement_list
284 285 286 287 288 289 290 291
    
    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
292

Jérome Perrin's avatar
Jérome Perrin committed
293 294
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')
295 296 297 298 299
    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
300 301
      return self.getMovementList(portal_type=
                          self.getPortalSimulatedMovementTypeList())
302

Jérome Perrin's avatar
Jérome Perrin committed
303 304
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
305 306 307 308 309
    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
310 311
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
312

Jérome Perrin's avatar
Jérome Perrin committed
313 314
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
315 316 317 318 319 320
    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
321 322
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
323 324 325
        container_list.append(m)
      return container_list

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

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

346
    security.declareProtected(Permissions.AccessContentsInformation, 'isDivergent')
347
    def isDivergent(self, fast=0, **kw):
348 349 350 351
      """
        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
352

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

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

373
    @UnrestrictedMethod
Sebastien Robin's avatar
Sebastien Robin committed
374
    def updateCausalityState(self, solve_automatically=True, **kw):
375 376 377 378 379
      """
      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
380 381 382 383
      isTransitionPossible = \
          self.getPortalObject().portal_workflow.isTransitionPossible
      if isTransitionPossible(self, 'diverge') and \
          isTransitionPossible(self, 'converge'):
384
        if self.isDivergent(**kw):
385
          if solve_automatically and \
Sebastien Robin's avatar
Sebastien Robin committed
386 387 388 389
              isTransitionPossible(self, 'solve_automatically'):
            self.solveAutomatically()
          else:
            self.diverge()
390 391
        else:
          self.converge()
392

393 394 395 396 397 398 399 400 401 402
    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)

403
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
404 405
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
      """
      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
424
      if start_date is not None or stop_date is not None:
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
        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:
440 441
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
442
      tag_list.append(solver_tag)
443
      kw = {'after_tag': tag_list[:], 'tag': expand_tag}
444
      for s_m in deferred_simulation_movement_list:
445
        s_m.expand('deferred', activate_kw=kw)
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 475 476 477 478
      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
479 480 481 482 483 484
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
485
      self.recursiveReindexObject(*k, **kw)
486
      # do not reexpand simulation: this is a task for DSolver / TSolver
Jean-Paul Smets's avatar
Jean-Paul Smets committed
487 488 489 490 491

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

499
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
500
                              'getInventory')
501 502 503 504 505 506
    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
507

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Romain Courteaud's avatar
Romain Courteaud committed
653 654 655 656




657 658 659 660 661 662 663 664 665
# 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)
666
#
667 668 669 670 671 672 673
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
674
#
675 676 677 678 679 680 681
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
682
#
683 684 685 686 687 688 689 690
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

691 692
    ##########################################################################
    # Applied Rule stuff
693

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
    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]
714
      else:
715 716
        after_tag = list(after_tag) if after_tag else []
      after_tag.append('expand:' + self.getPath())
717 718 719 720 721 722
      sm = getSecurityManager()
      newSecurityManager(None, nobody)
      try:
        unrestricted_apply(self.activate(after_tag=after_tag, **kw)._localBuild)
      finally:
        setSecurityManager(sm)
723

724 725 726
    def _localBuild(self):
      """Do an immediate local build for this delivery"""
      return self.asComposedDocument().build(explanation=self)
727

728 729 730
    def _createRootAppliedRule(self):
      portal = self.getPortalObject()
      # Only create RAR if we are not in a "too early" or "too late" state.
731 732 733
      state = self.getSimulationState()
      if (state != 'deleted' and
          state not in portal.getPortalDraftOrderStateList()):
734
        return super(Delivery, self)._createRootAppliedRule()
735 736

    security.declareProtected( Permissions.AccessContentsInformation,
737
                               'getRootCausalityValueList')
738 739 740 741 742 743
    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
      """
744 745 746
      causality_value_list = self.getCausalityValueList()
      if causality_value_list:
        initial_list = []
747
        for causality in causality_value_list:
748 749
          # The causality may be something which has not this method
          # (e.g. item)
750 751 752 753 754 755 756 757 758
          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]
759 760 761 762 763

    # 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,
764
                               'setRootCausalityValueList')
765
    def setRootCausalityValueList(self,value):
766
      """
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787
      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
788 789 790
      """
      pass

791 792 793
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
794 795 796 797 798
      # 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
799

800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
    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)

815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
    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
859 860 861

    def _getAllRelatedSimulationMovementList(self, **kw):
      movement_uid_list = [x.getUid() for x in self.getMovementList()]
862 863 864 865
      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
866 867 868 869 870 871 872

    def getDivergentTesterAndSimulationMovementList(self):
      """
      This method returns a list of (tester, simulation_movement) for each divergence.
      """
      divergent_tester_list = []
      for simulation_movement in self._getAllRelatedSimulationMovementList():
873
        simulation_movement = simulation_movement.getObject()
Sebastien Robin's avatar
Sebastien Robin committed
874 875 876 877 878
        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