Delivery.py 40.6 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 34
from Products.CMFCore.utils import getToolByName
from AccessControl import ClassSecurityInfo
35
from Products.ERP5Type import Permissions, PropertySheet, interfaces
36
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
37
from Products.ERP5Type.Errors import SimulationError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38
from Products.ERP5Type.XMLObject import XMLObject
39
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
40
from Products.ERP5.mixin.amount_generator import AmountGeneratorMixin
41
from Products.ERP5.mixin.composition import CompositionMixin
42
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43

44
from zLOG import LOG, PROBLEM
Jean-Paul Smets's avatar
Jean-Paul Smets committed
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

238
    security.declareProtected(Permissions.AccessContentsInformation,
239 240
                             '_getMovementList')
    def _getMovementList(self, portal_type=None, **kw):
241 242
      """
        Return a list of movements.
243 244
        First, we collect movements by movement type portal types, then
        we filter the result by specified portal types.
245
      """
246
      movement_portal_type_list = self.getPortalMovementTypeList()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
247
      movement_list = []
248
      add_movement = movement_list.append
249
      extend_movement = movement_list.extend
250 251
      sub_object_list = self.objectValues(\
          portal_type=movement_portal_type_list, **kw)
252 253 254 255
      extend_sub_object = sub_object_list.extend
      append_sub_object = sub_object_list.append
      while sub_object_list:
        sub_object = sub_object_list.pop()
256 257
        content_list = sub_object.objectValues(\
            portal_type=movement_portal_type_list, **kw)
258 259 260 261 262 263 264 265 266 267
        if sub_object.hasCellContent():
          cell_list = sub_object.getCellValueList()
          if len(cell_list) != len(content_list):
            for x in content_list:
              if x not in cell_list:
                append_sub_object(x)
          else:
            extend_movement(content_list)
        elif content_list:
          extend_sub_object(content_list)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
268
        else:
269
          add_movement(sub_object)
270 271 272 273 274 275
      if isinstance(portal_type, (list, tuple)):
        return [x for x in movement_list \
                if x.getPortalType() in portal_type]
      elif portal_type is not None:
        return [x for x in movement_list \
                if x.getPortalType() == portal_type]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
276
      return movement_list
277 278 279 280 281 282 283 284
    
    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
285

Jérome Perrin's avatar
Jérome Perrin committed
286 287
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')
288 289 290 291 292
    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
293 294
      return self.getMovementList(portal_type=
                          self.getPortalSimulatedMovementTypeList())
295

Jérome Perrin's avatar
Jérome Perrin committed
296 297
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
298 299 300 301 302
    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
303 304
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
305

Jérome Perrin's avatar
Jérome Perrin committed
306 307
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
308 309 310 311 312 313
    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
314 315
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
316 317 318
        container_list.append(m)
      return container_list

319
    def applyToDeliveryRelatedMovement(self, portal_type='Simulation Movement',
320
                                       method_id='expand', **kw):
Sebastien Robin's avatar
Sebastien Robin committed
321
      for simulation_movement in self._getAllRelatedSimulationMovementList():
322
        # And apply
Sebastien Robin's avatar
Sebastien Robin committed
323
        getattr(simulation_movement.getObject(), method_id)(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
324 325 326 327

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
334 335
    security.declareProtected(Permissions.View, 'isSimulated')
    def isSimulated(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
336 337 338 339
      """
        Returns 1 if all movements have a delivery or order counterpart
        in the simulation
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
340
      for m in self.getMovementList():
341 342
        #LOG('Delivery.isSimulated m',0,m.getPhysicalPath())
        #LOG('Delivery.isSimulated m.isSimulated',0,m.isSimulated())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
343
        if not m.isSimulated():
344 345
          #LOG('Delivery.isSimulated m.getQuantity',0,m.getQuantity())
          #LOG('Delivery.isSimulated m.getSimulationQuantity',0,m.getSimulationQuantity())
346
          if m.getQuantity() != 0.0 or m.getSimulationQuantity() not in (0, None):
347 348
            return 0
          # else Do we need to create a simulation movement ? XXX probably not
Jean-Paul Smets's avatar
Jean-Paul Smets committed
349
      return 1
350

351
    security.declareProtected(Permissions.View, 'isDivergent')
352
    def isDivergent(self, fast=0, **kw):
353 354 355 356
      """
        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
357

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

368
    security.declareProtected(Permissions.View, 'getDivergenceList')
369
    def getDivergenceList(self, **kw):
370 371 372 373
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
Sebastien Robin's avatar
Sebastien Robin committed
374 375
      for simulation_movement in self._getAllRelatedSimulationMovementList():
         divergence_list.extend(simulation_movement.getDivergenceList())
376 377
      return divergence_list

378
    @UnrestrictedMethod
Sebastien Robin's avatar
Sebastien Robin committed
379
    def updateCausalityState(self, solve_automatically=True, **kw):
380 381 382 383 384
      """
      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
385 386 387 388
      isTransitionPossible = \
          self.getPortalObject().portal_workflow.isTransitionPossible
      if isTransitionPossible(self, 'diverge') and \
          isTransitionPossible(self, 'converge'):
389
        if self.isDivergent(**kw):
Sebastien Robin's avatar
Sebastien Robin committed
390 391 392 393 394 395 396 397
          # If delivery is not simulated (PackingList.isDivergent()
          # returns True in such a case), we cannot solve divergence
          # anyway.
          if self.isSimulated() and solve_automatically and \
              isTransitionPossible(self, 'solve_automatically'):
            self.solveAutomatically()
          else:
            self.diverge()
398 399
        else:
          self.converge()
400

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

690 691
    ##########################################################################
    # Applied Rule stuff
692
    @UnrestrictedMethod # XXX-JPS What is this ?
693
    def updateAppliedRule(self, rule_reference=None, rule_id=None, **kw):
694
      """
695
      Create a new Applied Rule if none is related, or call expand
696
      on the existing one.
697 698

      The chosen applied rule will be the validated rule with reference ==
699
      rule_reference, and the higher version number.
700
      """
701 702 703 704 705 706
      if rule_id is not None:
        from warnings import warn
        warn('rule_id to updateAppliedRule is deprecated; use rule_reference instead',
             DeprecationWarning)
        rule_reference = rule_id

707
      if rule_reference is None:
708
        return
709 710 711 712 713 714

      # only expand if we are not in a "too early" or "too late" state
      if (self.getSimulationState() in
          self.getPortalDraftOrderStateList()):
        return

715
      portal_rules = getToolByName(self, 'portal_rules')
716
      res = portal_rules.searchFolder(reference=rule_reference,
717 718 719 720 721
          validation_state="validated", sort_on='version',
          sort_order='descending') # XXX validated is Hardcoded !

      if len(res) > 0:
        rule_id = res[0].getId()
722
      else:
723
        raise ValueError, 'No such rule as %r is found' % rule_reference
724

725
      self._createAppliedRule(rule_id, **kw)
726

727
    def _createAppliedRule(self, rule_id, activate_kw=None, **kw):
728 729 730 731 732
      """
        Create a new Applied Rule is none is related, or call expand
        on the existing one.
      """
      # Look up if existing applied rule
733 734
      my_applied_rule_list = self.getCausalityRelatedValueList(
          portal_type='Applied Rule')
735
      my_applied_rule = None
736
      if len(my_applied_rule_list) == 0:
737 738
        if self.isSimulated():
          # No need to create a DeliveryRule
739
          # if we are already in the simulation process
740 741 742 743 744 745
          pass
        else:
          # Create a new applied order rule (portal_rules.order_rule)
          portal_rules = getToolByName(self, 'portal_rules')
          portal_simulation = getToolByName(self, 'portal_simulation')
          my_applied_rule = portal_rules[rule_id].\
Jérome Perrin's avatar
Jérome Perrin committed
746 747
              constructNewAppliedRule(portal_simulation,
                                      activate_kw=activate_kw)
748 749 750 751
          # Set causality
          my_applied_rule.setCausalityValue(self)
          # We must make sure this rule is indexed
          # now in order not to create another one later
752
          my_applied_rule.reindexObject(activate_kw=activate_kw, **kw)
753 754 755 756
      elif len(my_applied_rule_list) == 1:
        # Re expand the rule if possible
        my_applied_rule = my_applied_rule_list[0]
      else:
757 758
        raise SimulationError('Delivery %s has more than one applied'
                              ' rule.' % self.getRelativeUrl())
759

760 761 762 763
      my_applied_rule_id = None
      expand_activate_kw = {}
      if my_applied_rule is not None:
        my_applied_rule_id = my_applied_rule.getId()
764 765 766
        expand_activate_kw['after_path_and_method_id'] = (
            my_applied_rule.getPath(),
            ['immediateReindexObject', 'recursiveImmediateReindexObject'])
767 768
      # We are now certain we have a single applied rule
      # It is time to expand it
769
      self.activate(activate_kw=activate_kw, **expand_activate_kw).expand(
770
          applied_rule_id=my_applied_rule_id,
771
          activate_kw=activate_kw, **kw)
772 773

    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
774
    @UnrestrictedMethod
775
    def expand(self, applied_rule_id=None, activate_kw=None,**kw):
776 777
      """
        Reexpand applied rule
778

779
        Also reexpand all rules related to movements
780 781

        NOTE: seems to be deprecated ?
782 783 784 785 786 787
      """
      excluded_rule_path_list = []
      if applied_rule_id is not None:
        my_applied_rule = self.portal_simulation.get(applied_rule_id, None)
        if my_applied_rule is not None:
          excluded_rule_path_list.append(my_applied_rule.getPath())
788
          my_applied_rule.expand(activate_kw=activate_kw,**kw)
789 790 791
          # once expanded, the applied_rule must be reindexed
          # because some simulation_movement may change even
          # if there are not edited (acquisition)
792 793 794 795 796 797 798 799 800
          #
          # XXX yo thinks that this is excessive. First of all, we may
          # need to reindex simulation movements but not applied rules
          # here. So we should skip reindexing applied rules.
          # In addition, the policy is "copy everything required to
          # simulation movements", so acquisitions should not matter to
          # indexing. The only exception is the simulation state.
          # I think, if each simulation movement remembers the previous
          # state, we can avoid unnecessary reindexing.
801
          my_applied_rule.recursiveReindexSimulationMovement(activate_kw=activate_kw)
802
        else:
803
          LOG("ERP5", PROBLEM,
804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
              "Could not expand applied rule %s for delivery %s" %\
                  (applied_rule_id, self.getId()))
      self.expandRuleRelatedToMovement(
                  excluded_rule_path_list=excluded_rule_path_list,
                  activate_kw=activate_kw,
                  **kw)

    security.declareProtected(Permissions.ModifyPortalContent,
        'expandRuleRelatedToMovement')
    def expandRuleRelatedToMovement(self,excluded_rule_path_list=None,
                                    activate_kw=None,**kw):
      """
      Some delivery movement may be related to another applied rule than
      the one related to the delivery. Delivery movements may be related
      to many simulation movements from many different root applied rules,
      so it is required to expand the applied rule parent to related
      simulation movements.

      exclude_rule_path : do not expand this applied rule (or children
                          applied rule)
      """
      if excluded_rule_path_list is None:
        excluded_rule_path_list = []
      to_expand_list = []
Sebastien Robin's avatar
Sebastien Robin committed
828 829 830 831 832 833
      for sim_movement in self._getAllRelatedSimulationMovementList():
        if sim_movement.getRootAppliedRule().getPath() \
            not in excluded_rule_path_list:
          parent_value = sim_movement.getParentValue()
          if parent_value not in to_expand_list:
            to_expand_list.append(parent_value)
834
      for rule in to_expand_list:
835 836
        rule.expand(activate_kw=activate_kw, **kw)
        rule.recursiveReindexSimulationMovement(activate_kw=activate_kw)
837 838

    security.declareProtected( Permissions.AccessContentsInformation,
839
                               'getRootCausalityValueList')
840 841 842 843 844 845
    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
      """
846 847
      causality_value_list = [x for x in self.getCausalityValueList()
                                if x is not self]
848 849 850 851 852
      initial_list = []
      if len(causality_value_list)==0:
        initial_list = [self]
      else:
        for causality in causality_value_list:
853 854 855 856
          # The causality may be something which has not this method
          # (e.g. item)
          if hasattr(causality, 'getRootCausalityValueList'):
            tmp_causality_list = causality.getRootCausalityValueList()
857
            initial_list.extend([x for x in tmp_causality_list
858
                                 if x not in initial_list])
859 860 861 862 863 864 865
      return initial_list


    # 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,
866
                               'setRootCausalityValueList')
867
    def setRootCausalityValueList(self,value):
868
      """
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889
      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
890 891 892
      """
      pass

893 894 895
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
896 897 898 899 900
      # 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
901 902 903 904 905 906 907

    def getRuleReference(self):
      """Returns an appropriate rule reference."""
      method = self._getTypeBasedMethod('getRuleReference')
      if method is not None:
        return method()
      else:
908 909
        raise SimulationError('%s_getRuleReference script is missing.'
                              % self.getPortalType().replace(' ', ''))
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925

    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)

926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
    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
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991

    def _getAllRelatedSimulationMovementList(self, **kw):
      search_method = \
          self.getPortalObject().portal_catalog.unrestrictedSearchResults
      movement_uid_list = [x.getUid() for x in self.getMovementList()]
      if len(movement_uid_list) == 0:
        return []
      sim_movement_list = search_method(portal_type='Simulation Movement',
                                        delivery_uid=movement_uid_list, **kw)
      return sim_movement_list

    def getDivergentTesterAndSimulationMovementList(self):
      """
      This method returns a list of (tester, simulation_movement) for each divergence.
      """
      divergent_tester_list = []
      for simulation_movement in self._getAllRelatedSimulationMovementList():
        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