Delivery.py 34.9 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
3
# Copyright (c) 2002-2008 Nexedi SA and Contributors. All Rights Reserved.
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Romain Courteaud's avatar
Romain Courteaud committed
5
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7
#
# WARNING: This program as such is intended to be used by professional
8
# programmers who take the whole responsibility of assessing all potential
Jean-Paul Smets's avatar
Jean-Paul Smets committed
9 10
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
11
# guarantees and support are strongly adviced to contract a Free Software
Jean-Paul Smets's avatar
Jean-Paul Smets committed
12 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.
#
##############################################################################

from Products.CMFCore.utils import getToolByName
31
from Products.ERP5Type.Base import WorkflowMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33 34
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5Type.XMLObject import XMLObject
35
from Products.ERP5.Document.Movement import Movement
36
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
37
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38

39
from zLOG import LOG, PROBLEM
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40

41
class Delivery(XMLObject, ImmobilisationDelivery):
42 43 44 45
    """
        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
46 47 48 49 50
    # CMF Type Definition
    meta_type = 'ERP5 Delivery'
    portal_type = 'Delivery'
    isPortalContent = 1
    isRADContent = 1
51
    isDelivery = 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
52 53 54

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

    # Default Properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.XMLObject
                      , PropertySheet.CategoryCore
                      , PropertySheet.DublinCore
                      , PropertySheet.Task
                      , PropertySheet.Arrow
                      , PropertySheet.Movement
                      , PropertySheet.Delivery
                      , PropertySheet.Reference
67
                      , PropertySheet.Price
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
                      )

    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
92
    security.declareProtected(Permissions.AccessContentsInformation, 'getDefaultTotalPrice')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
    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))

110 111
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getTotalPrice')
112
    def getTotalPrice(self, fast=0, src__=0, **kw):
113 114 115 116
      """ 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
117 118 119

        So if the order is not in the catalog, getTotalPrice(fast=1)
        will return 0, this is not a bug.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
120
      """
121 122 123 124 125
      if not fast :
        kw.setdefault( 'portal_type',
                       self.getPortalDeliveryMovementTypeList())
        return sum([ line.getTotalPrice(fast=0) for line in
                        self.objectValues(**kw) ])
126
      kw['explanation_uid'] = self.getUid()
127 128
      kw.update(self.portal_catalog.buildSQLQuery(**kw))
      if src__:
129 130
        return self.Delivery_zGetTotal(src__=1, **kw)
      aggregate = self.Delivery_zGetTotal(**kw)[0]
131
      return aggregate.total_price or 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
132

133 134 135 136 137 138 139 140 141 142
    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)

143
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
144
                              'getTotalQuantity')
145
    def getTotalQuantity(self, fast=0, src__=0, **kw):
146 147 148 149
      """ 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
150 151 152

        So if the order is not in the catalog, getTotalQuantity(fast=1)
        will return 0, this is not a bug.
153
      """
154
      if not fast :
Romain Courteaud's avatar
Romain Courteaud committed
155 156
        kw.setdefault('portal_type',
                      self.getPortalDeliveryMovementTypeList())
157 158
        return sum([ line.getTotalQuantity(fast=0) for line in
                        self.objectValues(**kw) ])
159
      kw['explanation_uid'] = self.getUid()
160 161
      kw.update(self.portal_catalog.buildSQLQuery(**kw))
      if src__:
162 163
        return self.Delivery_zGetTotal(src__=1, **kw)
      aggregate = self.Delivery_zGetTotal(**kw)[0]
164
      return aggregate.total_quantity or 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
165

Jérome Perrin's avatar
Jérome Perrin committed
166 167
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
168 169 170
    def getDeliveryUid(self):
      return self.getUid()

Jérome Perrin's avatar
Jérome Perrin committed
171 172
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryValue')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
173
    def getDeliveryValue(self):
174 175 176 177 178
      """
      Deprecated, we should use getRootDeliveryValue instead
      """
      return self

Jérome Perrin's avatar
Jérome Perrin committed
179 180
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')
181 182 183 184 185
    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
186 187
      return self

Jérome Perrin's avatar
Jérome Perrin committed
188 189
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')
190 191 192
    def getDelivery(self):
      return self.getRelativeUrl()

193
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
194 195
                             'getMovementList')
    def getMovementList(self, portal_type=None, **kw):
196 197 198
      """
        Return a list of movements.
      """
199 200
      if portal_type is None:
        portal_type = self.getPortalMovementTypeList()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
201
      movement_list = []
202
      add_movement = movement_list.append
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
      extend_movement = movement_list.extend
      sub_object_list = self.contentValues(filter={'portal_type': portal_type})
      extend_sub_object = sub_object_list.extend
      append_sub_object = sub_object_list.append
      while sub_object_list:
        sub_object = sub_object_list.pop()
        content_list = sub_object.contentValues(
                          filter={'portal_type': portal_type})
        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
221
        else:
222
          add_movement(sub_object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
223 224
      return movement_list

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

Jérome Perrin's avatar
Jérome Perrin committed
235 236
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
237 238 239 240 241
    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
242 243
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
244

Jérome Perrin's avatar
Jérome Perrin committed
245 246
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
247 248 249 250 251 252
    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
253 254
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
255 256 257
        container_list.append(m)
      return container_list

258
    def applyToDeliveryRelatedMovement(self, portal_type='Simulation Movement',
259
                                       method_id='expand', **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
260
      for my_simulation_movement in self.getDeliveryRelatedValueList(
Jérome Perrin's avatar
Jérome Perrin committed
261
                                      portal_type = 'Simulation Movement'):
262 263 264 265
        # And apply
        getattr(my_simulation_movement.getObject(), method_id)(**kw)

      for m in self.getMovementList():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
266 267
        # Find related in simulation
        for my_simulation_movement in m.getDeliveryRelatedValueList(
Jérome Perrin's avatar
Jérome Perrin committed
268
                                  portal_type = 'Simulation Movement'):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
269
          # And apply
270
          getattr(my_simulation_movement.getObject(), method_id)(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
271 272 273 274

    #######################################################
    # Causality computation
    security.declareProtected(Permissions.View, 'isConvergent')
275
    def isConvergent(self,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
276 277 278
      """
        Returns 0 if the target is not met
      """
279
      return int(not self.isDivergent(**kw))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
280

Jean-Paul Smets's avatar
Jean-Paul Smets committed
281 282
    security.declareProtected(Permissions.View, 'isSimulated')
    def isSimulated(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
283 284 285 286
      """
        Returns 1 if all movements have a delivery or order counterpart
        in the simulation
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
287
      for m in self.getMovementList():
288 289
        #LOG('Delivery.isSimulated m',0,m.getPhysicalPath())
        #LOG('Delivery.isSimulated m.isSimulated',0,m.isSimulated())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
290
        if not m.isSimulated():
291 292
          #LOG('Delivery.isSimulated m.getQuantity',0,m.getQuantity())
          #LOG('Delivery.isSimulated m.getSimulationQuantity',0,m.getSimulationQuantity())
293
          if m.getQuantity() != 0.0 or m.getSimulationQuantity() != 0:
294 295
            return 0
          # else Do we need to create a simulation movement ? XXX probably not
Jean-Paul Smets's avatar
Jean-Paul Smets committed
296
      return 1
297

298
    security.declareProtected(Permissions.View, 'isDivergent')
299
    def isDivergent(self, fast=0, **kw):
300 301 302 303
      """
        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
304

305 306
        emit targetUnreachable !
      """
307 308 309 310
      # Delivery_zIsDivergent only works when object and simulation is
      # reindexed, so if an user change the delivery, he must wait
      # until everything is indexed, this is not acceptable for users
      # so we should not use it by default (and may be we should remove)
311
      if fast==1 and len(self.Delivery_zIsDivergent(uid=self.getUid())) > 0:
312 313
        return 1
      # Check if the total quantity equals the total of each simulation movement quantity
314 315 316
      for movement in self.getMovementList():
        if movement.isDivergent():
          return 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
317 318
      return 0

319
    security.declareProtected(Permissions.View, 'getDivergenceList')
320
    def getDivergenceList(self, **kw):
321 322 323 324 325 326 327 328
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
      for movement in self.getMovementList():
         divergence_list.extend(movement.getDivergenceList())
      return divergence_list

329
    def updateCausalityState(self, **kw):
330 331 332 333 334
      """
      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
      """
335 336 337 338
      updateCausalityState = UnrestrictedMethod(self._updateCausalityState)
      return updateCausalityState(**kw)

    def _updateCausalityState(self, **kw):
339 340
      if getattr(self, 'diverge', None) is not None \
            and getattr(self, 'converge', None) is not None:
341
        if self.isDivergent(**kw):
342 343 344
          self.diverge()
        else:
          self.converge()
345

346
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
347 348
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
      """
      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
      if start_date != None or stop_date != None:
        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:
383 384
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
      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
421 422 423 424 425 426
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
427
      self.recursiveReindexObject(*k, **kw)
428 429 430
      # 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
431 432 433 434 435

    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
      resource_dict = {}
Romain Courteaud's avatar
Romain Courteaud committed
436 437
      for m in self.contentValues(filter={
                      'portal_type': self.getPortalMovementTypeList()}):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
438 439 440 441 442
        r = m.getResource()
        if r is not None:
          resource_dict[r] = 1
      return resource_dict.keys()

443
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
444
                              'getInventory')
445 446 447 448 449 450
    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
451

452
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
453
                              'getCurrentInventory')
454
    def getCurrentInventory(self, **kw):
455 456 457
      """
      Returns current inventory
      """
Romain Courteaud's avatar
Romain Courteaud committed
458
      kw['resource'] = self._getMovementResourceList()
459
      return self.portal_simulation.getCurrentInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
460

461
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
462
                              'getAvailableInventory')
463
    def getAvailableInventory(self, **kw):
464 465 466 467
      """
      Returns available inventory
      (current inventory - deliverable)
      """
Romain Courteaud's avatar
Romain Courteaud committed
468
      kw['resource'] = self._getMovementResourceList()
469 470
      return self.portal_simulation.getAvailableInventory(**kw)

471
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
472
                              'getFutureInventory')
473
    def getFutureInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
474
      """
475
      Returns inventory at infinite
Jean-Paul Smets's avatar
Jean-Paul Smets committed
476
      """
Romain Courteaud's avatar
Romain Courteaud committed
477
      kw['resource'] = self._getMovementResourceList()
478
      return self.portal_simulation.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
479

480
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
481
                              'getInventoryList')
482
    def getInventoryList(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
483
      """
484
      Returns list of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
485
      """
Romain Courteaud's avatar
Romain Courteaud committed
486
      kw['resource'] = self._getMovementResourceList()
487
      return self.portal_simulation.getInventoryList(**kw)
488

489
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
490
                              'getCurrentInventoryList')
491
    def getCurrentInventoryList(self, **kw):
492 493 494
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
495
      kw['resource'] = self._getMovementResourceList()
496
      return self.portal_simulation.getCurrentInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
497

498
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
499
                              'getFutureInventoryList')
500
    def getFutureInventoryList(self, **kw):
501 502 503
      """
      Returns list of inventory grouped by section or site
      """
Romain Courteaud's avatar
Romain Courteaud committed
504
      kw['resource'] = self._getMovementResourceList()
505
      return self.portal_simulation.getFutureInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
506

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

516
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
517
                              'getCurrentInventoryStat')
518
    def getCurrentInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
519
      """
520
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
521
      """
Romain Courteaud's avatar
Romain Courteaud committed
522
      kw['resource'] = self._getMovementResourceList()
523
      return self.portal_simulation.getCurrentInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
524

525
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
526
                              'getFutureInventoryStat')
527
    def getFutureInventoryStat(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
528
      """
529
      Returns statistics of inventory grouped by section or site
Jean-Paul Smets's avatar
Jean-Paul Smets committed
530
      """
Romain Courteaud's avatar
Romain Courteaud committed
531
      kw['resource'] = self._getMovementResourceList()
532
      return self.portal_simulation.getFutureInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
533

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

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

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

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

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

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

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

Romain Courteaud's avatar
Romain Courteaud committed
597 598 599 600




601 602 603 604 605 606 607 608 609
# 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)
610
#
611 612 613 614 615 616 617
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
618
#
619 620 621 622 623 624 625
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
626
#
627 628 629 630 631 632 633 634
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

635 636 637 638 639 640 641 642 643 644 645
    security.declarePrivate( '_edit' )
    def _edit(self, REQUEST=None, force_update = 0, **kw):
      """
      call propagateArrowToSimulation
      """
      XMLObject._edit(self,REQUEST=REQUEST,force_update=force_update,**kw)
      #self.propagateArrowToSimulation()
      # We must expand our applied rule only if not confirmed
      #if self.getSimulationState() in planned_order_state:
      #  self.updateAppliedRule() # This should be implemented with the interaction tool rather than with this hard coding

646 647 648 649 650 651 652 653 654
    security.declareProtected(Permissions.ModifyPortalContent, 'notifySimulationChange')
    def notifySimulationChange(self):
      """
        WorkflowMethod used to notify the causality workflow that the simulation
        has changed, so we have to check if the delivery is divergent or not
      """
      pass
    notifySimulationChange = WorkflowMethod(notifySimulationChange)

655 656
    ##########################################################################
    # Applied Rule stuff
657
    def updateAppliedRule(self, *args, **kw):
658
      """
659
      Create a new Applied Rule if none is related, or call expand
660
      on the existing one.
661 662

      The chosen applied rule will be the validated rule with reference ==
663
      rule_reference, and the higher version number.
664

665 666
      If no rule is found, simply pass rule_reference to _createAppliedRule,
      to keep compatibility vith the previous behaviour
667
      """
668 669 670 671 672
      updateAppliedRule = UnrestrictedMethod(self._updateAppliedRule)
      return updateAppliedRule(*args, **kw)

    def _updateAppliedRule(self, rule_reference=None, rule_id=None, force=0,
                           **kw):
673 674 675 676 677 678
      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

679
      if rule_reference is None:
680 681 682
        return

      portal_rules = getToolByName(self, 'portal_rules')
683
      res = portal_rules.searchFolder(reference=rule_reference,
684 685 686 687 688
          validation_state="validated", sort_on='version',
          sort_order='descending') # XXX validated is Hardcoded !

      if len(res) > 0:
        rule_id = res[0].getId()
689 690
      else:
        rule_id = rule_reference
691

692
      # only expand if we are not in a "too early" or "too late" state
693 694 695
      if (self.getSimulationState() not in
          self.getPortalDraftOrderStateList()):
        self._createAppliedRule(rule_id, force=force, **kw)
696

697
    def _createAppliedRule(self, rule_id, force=0, activate_kw=None, **kw):
698 699 700 701 702
      """
        Create a new Applied Rule is none is related, or call expand
        on the existing one.
      """
      # Look up if existing applied rule
703 704
      my_applied_rule_list = self.getCausalityRelatedValueList(
          portal_type='Applied Rule')
705
      my_applied_rule = None
706
      if len(my_applied_rule_list) == 0:
707 708
        if self.isSimulated():
          # No need to create a DeliveryRule
709
          # if we are already in the simulation process
710 711 712 713 714 715
          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].\
716
              constructNewAppliedRule(portal_simulation)
717 718 719 720
          # Set causality
          my_applied_rule.setCausalityValue(self)
          # We must make sure this rule is indexed
          # now in order not to create another one later
721
          my_applied_rule.reindexObject(activate_kw=activate_kw, **kw)
722 723 724 725
      elif len(my_applied_rule_list) == 1:
        # Re expand the rule if possible
        my_applied_rule = my_applied_rule_list[0]
      else:
Jérome Perrin's avatar
Jérome Perrin committed
726
        raise "SimulationError", 'Delivery %s has more than one applied'\
727
            ' rule.' % self.getRelativeUrl()
728

729 730 731 732
      my_applied_rule_id = None
      expand_activate_kw = {}
      if my_applied_rule is not None:
        my_applied_rule_id = my_applied_rule.getId()
733 734 735
        expand_activate_kw['after_path_and_method_id'] = (
            my_applied_rule.getPath(),
            ['immediateReindexObject', 'recursiveImmediateReindexObject'])
736 737
      # We are now certain we have a single applied rule
      # It is time to expand it
738 739 740
      self.activate(activate_kw=activate_kw, **expand_activate_kw).expand(
          applied_rule_id=my_applied_rule_id, force=force,
          activate_kw=activate_kw, **kw)
741 742

    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
743
    def expand(self, *args,**kw):
744 745
      """
        Reexpand applied rule
746

747 748
        Also reexpand all rules related to movements
      """
749 750 751 752
      expand = UnrestrictedMethod(self._expand)
      return expand(*args, **kw)

    def _expand(self, applied_rule_id=None, force=0, activate_kw=None,**kw):
753 754 755 756 757 758 759 760 761 762 763
      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())
          my_applied_rule.expand(force=force, activate_kw=activate_kw,**kw)
          # once expanded, the applied_rule must be reindexed
          # because some simulation_movement may change even
          # if there are not edited (acquisition)
          my_applied_rule.recursiveReindexObject(activate_kw=activate_kw)
        else:
764
          LOG("ERP5", PROBLEM,
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
              "Could not expand applied rule %s for delivery %s" %\
                  (applied_rule_id, self.getId()))
      self.expandRuleRelatedToMovement(
                  excluded_rule_path_list=excluded_rule_path_list,
                  force=force,
                  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 = []
      # we might use a zsql method, because it can be very slow
      for m in self.getMovementList():
        if m.isSimulated():
          sim_movement_list = m.getDeliveryRelatedValueList()
          for sim_movement in sim_movement_list:
            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)
      for rule in to_expand_list:
        rule.expand(activate_kw=activate_kw,**kw)
        rule.recursiveReindexObject(activate_kw=activate_kw)
803 804

    security.declareProtected( Permissions.AccessContentsInformation,
805
                               'getRootCausalityValueList')
806 807 808 809 810 811
    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
      """
812 813
      causality_value_list = [x for x in self.getCausalityValueList()
                                if x is not self]
814 815 816 817 818
      initial_list = []
      if len(causality_value_list)==0:
        initial_list = [self]
      else:
        for causality in causality_value_list:
819 820 821 822
          # The causality may be something which has not this method
          # (e.g. item)
          if hasattr(causality, 'getRootCausalityValueList'):
            tmp_causality_list = causality.getRootCausalityValueList()
823
            initial_list.extend([x for x in tmp_causality_list
824
                                 if x not in initial_list])
825 826 827 828 829 830 831
      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,
832
                               'setRootCausalityValueList')
833
    def setRootCausalityValueList(self,value):
834
      """
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
      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
856 857 858
      """
      pass

859 860 861 862 863 864 865 866 867 868 869 870
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()

    def getRuleReference(self):
      """Returns an appropriate rule reference."""
      method = self._getTypeBasedMethod('getRuleReference')
      if method is not None:
        return method()
      else:
        raise 'SimulationError', '%s_getRuleReference script is missing.' \
              % self.getPortalType()