Delivery.py 35.4 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
    def updateSimulation(self, calculate=False, **kw):
      if calculate:
        path = self.getPath()
        self.activate(
397
          # after_tag to built: could be removed
398 399 400 401 402 403
          after_tag=('built:'+path, 'expand:'+path),
          after_path_and_method_id=(path, '_localBuild'),
          ).updateCausalityState()
      if kw:
        super(Delivery, self).updateSimulation(**kw)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

692 693
    ##########################################################################
    # Applied Rule stuff
694

695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
    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]
715
      else:
716
        after_tag = list(after_tag) if after_tag else []
717 718 719 720
      # Now that 'delivery' category relation are indexed in ZODB, this is the
      # only method that depends on built: tag (via _updateSimulation), which
      # is still required because builders only use catalog to find buildable
      # movements and we don't want to miss any for local building.
721
      after_tag.append('expand:' + self.getPath())
722 723 724 725 726 727
      sm = getSecurityManager()
      newSecurityManager(None, nobody)
      try:
        unrestricted_apply(self.activate(after_tag=after_tag, **kw)._localBuild)
      finally:
        setSecurityManager(sm)
728

729 730 731
    def _localBuild(self):
      """Do an immediate local build for this delivery"""
      return self.asComposedDocument().build(explanation=self)
732

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

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

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

796 797 798
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
799 800 801 802 803
      # 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
804

805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
    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)

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 859 860 861 862 863
    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
864

865 866 867 868 869
    def _getAllRelatedSimulationMovementList(self):
      result = []
      for movement in self.getMovementList():
        result += movement.getDeliveryRelatedValueList()
      return result
Sebastien Robin's avatar
Sebastien Robin committed
870 871 872 873 874 875 876

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