Delivery.py 37.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4
# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Romain Courteaud's avatar
Romain Courteaud committed
6
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
7 8
#
# WARNING: This program as such is intended to be used by professional
9
# programmers who take the whole responsibility of assessing all potential
Jean-Paul Smets's avatar
Jean-Paul Smets committed
10 11
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
12
# guarantees and support are strongly adviced to contract a Free Software
Jean-Paul Smets's avatar
Jean-Paul Smets committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

31 32
import zope.interface

Jean-Paul Smets's avatar
Jean-Paul Smets committed
33
from Products.CMFCore.utils import getToolByName
34
from Products.ERP5Type.Base import WorkflowMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
35
from AccessControl import ClassSecurityInfo
36
from Products.ERP5Type import Permissions, PropertySheet, interfaces
37
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
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.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
41

42
from zLOG import LOG, PROBLEM
43
from zExceptions import NotFound
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44

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

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

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

72 73 74
    # Declarative interfaces
    zope.interface.implements(interfaces.IDivergenceController,)

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

115 116
    security.declareProtected( Permissions.AccessContentsInformation,
                               'getTotalPrice')
117
    def getTotalPrice(self, fast=0, src__=0, **kw):
118 119 120 121
      """ 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
122 123 124

        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
125
      """
126 127
      result = None
      if not fast:
128 129
        kw.setdefault( 'portal_type',
                       self.getPortalDeliveryMovementTypeList())
130 131 132 133 134 135 136 137 138
        result = sum([ line.getTotalPrice(fast=0) for line in
                       self.objectValues(**kw) ])
      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
139
      method = self._getTypeBasedMethod('convertTotalPrice')
140
      if method is not None:
141 142
        return method(result)
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
143

144 145 146 147 148 149 150 151 152 153
    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)

154
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
155
                              'getTotalQuantity')
156
    def getTotalQuantity(self, fast=0, src__=0, **kw):
157 158 159 160
      """ 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
161 162 163

        So if the order is not in the catalog, getTotalQuantity(fast=1)
        will return 0, this is not a bug.
164
      """
165
      if not fast :
Romain Courteaud's avatar
Romain Courteaud committed
166 167
        kw.setdefault('portal_type',
                      self.getPortalDeliveryMovementTypeList())
168 169
        return sum([ line.getTotalQuantity(fast=0) for line in
                        self.objectValues(**kw) ])
170
      kw['explanation_uid'] = self.getUid()
171 172
      kw.update(self.portal_catalog.buildSQLQuery(**kw))
      if src__:
173 174
        return self.Delivery_zGetTotal(src__=1, **kw)
      aggregate = self.Delivery_zGetTotal(**kw)[0]
175
      return aggregate.total_quantity or 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
176

Jérome Perrin's avatar
Jérome Perrin committed
177 178
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
179 180 181
    def getDeliveryUid(self):
      return self.getUid()

Jérome Perrin's avatar
Jérome Perrin committed
182 183
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryValue')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
184
    def getDeliveryValue(self):
185 186 187 188 189
      """
      Deprecated, we should use getRootDeliveryValue instead
      """
      return self

Jérome Perrin's avatar
Jérome Perrin committed
190 191
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')
192 193 194 195 196
    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
197 198
      return self

Jérome Perrin's avatar
Jérome Perrin committed
199 200
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')
201 202 203
    def getDelivery(self):
      return self.getRelativeUrl()

204
    security.declareProtected(Permissions.AccessContentsInformation,
205 206
                             '_getMovementList')
    def _getMovementList(self, portal_type=None, **kw):
207 208
      """
        Return a list of movements.
209 210
        First, we collect movements by movement type portal types, then
        we filter the result by specified portal types.
211
      """
212
      movement_portal_type_list = self.getPortalMovementTypeList()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
213
      movement_list = []
214
      add_movement = movement_list.append
215
      extend_movement = movement_list.extend
216
      sub_object_list = self.objectValues(portal_type=movement_portal_type_list)
217 218 219 220
      extend_sub_object = sub_object_list.extend
      append_sub_object = sub_object_list.append
      while sub_object_list:
        sub_object = sub_object_list.pop()
221
        content_list = sub_object.objectValues(portal_type=movement_portal_type_list)
222 223 224 225 226 227 228 229 230 231
        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
232
        else:
233
          add_movement(sub_object)
234 235 236 237 238 239
      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
240
      return movement_list
241 242 243 244 245 246 247 248
    
    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
249

Jérome Perrin's avatar
Jérome Perrin committed
250 251
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')
252 253 254 255 256
    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
257 258
      return self.getMovementList(portal_type=
                          self.getPortalSimulatedMovementTypeList())
259

Jérome Perrin's avatar
Jérome Perrin committed
260 261
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
262 263 264 265 266
    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
267 268
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
269

Jérome Perrin's avatar
Jérome Perrin committed
270 271
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
272 273 274 275 276 277
    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
278 279
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
280 281 282
        container_list.append(m)
      return container_list

283
    def applyToDeliveryRelatedMovement(self, portal_type='Simulation Movement',
284
                                       method_id='expand', **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
285
      for my_simulation_movement in self.getDeliveryRelatedValueList(
Jérome Perrin's avatar
Jérome Perrin committed
286
                                      portal_type = 'Simulation Movement'):
287 288 289 290
        # And apply
        getattr(my_simulation_movement.getObject(), method_id)(**kw)

      for m in self.getMovementList():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
291 292
        # Find related in simulation
        for my_simulation_movement in m.getDeliveryRelatedValueList(
Jérome Perrin's avatar
Jérome Perrin committed
293
                                  portal_type = 'Simulation Movement'):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
294
          # And apply
295
          getattr(my_simulation_movement.getObject(), method_id)(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
296 297 298 299

    #######################################################
    # Causality computation
    security.declareProtected(Permissions.View, 'isConvergent')
300
    def isConvergent(self,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
301 302 303
      """
        Returns 0 if the target is not met
      """
304
      return int(not self.isDivergent(**kw))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
305

Jean-Paul Smets's avatar
Jean-Paul Smets committed
306 307
    security.declareProtected(Permissions.View, 'isSimulated')
    def isSimulated(self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
308 309 310 311
      """
        Returns 1 if all movements have a delivery or order counterpart
        in the simulation
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
312
      for m in self.getMovementList():
313 314
        #LOG('Delivery.isSimulated m',0,m.getPhysicalPath())
        #LOG('Delivery.isSimulated m.isSimulated',0,m.isSimulated())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
315
        if not m.isSimulated():
316 317
          #LOG('Delivery.isSimulated m.getQuantity',0,m.getQuantity())
          #LOG('Delivery.isSimulated m.getSimulationQuantity',0,m.getSimulationQuantity())
318
          if m.getQuantity() != 0.0 or m.getSimulationQuantity() != 0:
319 320
            return 0
          # else Do we need to create a simulation movement ? XXX probably not
Jean-Paul Smets's avatar
Jean-Paul Smets committed
321
      return 1
322

323
    security.declareProtected(Permissions.View, 'isDivergent')
324
    def isDivergent(self, fast=0, **kw):
325 326 327 328
      """
        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
329

330 331
        emit targetUnreachable !
      """
332 333
      ## Note that fast option was removed. Now, fast=1 is ignored.
      
334
      # Check if the total quantity equals the total of each simulation movement quantity
335 336 337
      for movement in self.getMovementList():
        if movement.isDivergent():
          return 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
338 339
      return 0

340
    security.declareProtected(Permissions.View, 'getDivergenceList')
341
    def getDivergenceList(self, **kw):
342 343 344 345 346 347 348 349
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
      for movement in self.getMovementList():
         divergence_list.extend(movement.getDivergenceList())
      return divergence_list

350
    @UnrestrictedMethod
351
    def updateCausalityState(self, **kw):
352 353 354 355 356
      """
      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
      """
357 358
      if getattr(self, 'diverge', None) is not None \
            and getattr(self, 'converge', None) is not None:
359
        if self.isDivergent(**kw):
360 361 362
          self.diverge()
        else:
          self.converge()
363

364
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
365 366
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
      """
      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
385
      if start_date is not None or stop_date is not None:
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
        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:
401 402
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
      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
439 440 441 442 443 444
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
445
      self.recursiveReindexObject(*k, **kw)
446 447 448
      # 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
449 450 451 452 453

    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
      resource_dict = {}
Romain Courteaud's avatar
Romain Courteaud committed
454 455
      for m in self.contentValues(filter={
                      'portal_type': self.getPortalMovementTypeList()}):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
456 457 458 459 460
        r = m.getResource()
        if r is not None:
          resource_dict[r] = 1
      return resource_dict.keys()

461
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
462
                              'getInventory')
463 464 465 466 467 468
    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
469

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

479
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
480
                              'getAvailableInventory')
481
    def getAvailableInventory(self, **kw):
482 483 484 485
      """
      Returns available inventory
      (current inventory - deliverable)
      """
Romain Courteaud's avatar
Romain Courteaud committed
486
      kw['resource'] = self._getMovementResourceList()
487 488
      return self.portal_simulation.getAvailableInventory(**kw)

489
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
490
                              'getFutureInventory')
491
    def getFutureInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
492
      """
493
      Returns inventory at infinite
Jean-Paul Smets's avatar
Jean-Paul Smets committed
494
      """
Romain Courteaud's avatar
Romain Courteaud committed
495
      kw['resource'] = self._getMovementResourceList()
496
      return self.portal_simulation.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
497

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

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

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

525
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
526
                              'getInventoryStat')
527
    def getInventoryStat(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.getInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
533

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

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

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

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

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

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

588
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
589
                              'getInventoryHistoryChart')
590
    def getInventoryHistoryChart(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.getInventoryHistoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
596

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

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

Romain Courteaud's avatar
Romain Courteaud committed
615 616 617 618




619 620 621 622 623 624 625 626 627
# 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)
628
#
629 630 631 632 633 634 635
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
636
#
637 638 639 640 641 642 643
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
644
#
645 646 647 648 649 650 651 652
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

653 654 655 656 657 658 659 660 661 662 663
    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

664 665
    ##########################################################################
    # Applied Rule stuff
666 667 668
    @UnrestrictedMethod
    def updateAppliedRule(self, rule_reference=None, rule_id=None, force=0,
                          **kw):
669
      """
670
      Create a new Applied Rule if none is related, or call expand
671
      on the existing one.
672 673

      The chosen applied rule will be the validated rule with reference ==
674
      rule_reference, and the higher version number.
675
      """
676 677 678 679 680 681
      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

682
      if rule_reference is None:
683
        return
684 685 686 687 688 689

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

690
      portal_rules = getToolByName(self, 'portal_rules')
691
      res = portal_rules.searchFolder(reference=rule_reference,
692 693 694 695 696
          validation_state="validated", sort_on='version',
          sort_order='descending') # XXX validated is Hardcoded !

      if len(res) > 0:
        rule_id = res[0].getId()
697
      else:
698
        raise NotFound('No such rule as %r is found' % (rule_reference,))
699

700
      self._createAppliedRule(rule_id, force=force, **kw)
701

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

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

    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
748 749
    @UnrestrictedMethod
    def expand(self, applied_rule_id=None, force=0, activate_kw=None,**kw):
750 751
      """
        Reexpand applied rule
752

753 754 755 756 757 758 759 760 761 762 763 764 765
        Also reexpand all rules related to movements
      """
      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:
766
          LOG("ERP5", PROBLEM,
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 803 804
              "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)
805 806

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

861 862 863
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
864 865 866 867 868
      # 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
869 870 871 872 873 874 875 876 877

    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()
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893

    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)

894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
    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