Delivery.py 37.8 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
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

363
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
364 365
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
      """
      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
384
      if start_date is not None or stop_date is not None:
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
        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:
400 401
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
402 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
      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
438 439 440 441 442 443
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
444
      self.recursiveReindexObject(*k, **kw)
445 446 447
      # 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
448 449 450 451 452

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

701
    def _createAppliedRule(self, rule_id, force=0, activate_kw=None, **kw):
702 703 704 705 706
      """
        Create a new Applied Rule is none is related, or call expand
        on the existing one.
      """
      # Look up if existing applied rule
707 708
      my_applied_rule_list = self.getCausalityRelatedValueList(
          portal_type='Applied Rule')
709
      my_applied_rule = None
710
      if len(my_applied_rule_list) == 0:
711 712
        if self.isSimulated():
          # No need to create a DeliveryRule
713
          # if we are already in the simulation process
714 715 716 717 718 719
          pass
        else:
          # Create a new applied order rule (portal_rules.order_rule)
          portal_rules = getToolByName(self, 'portal_rules')
          portal_simulation = getToolByName(self, 'portal_simulation')
          my_applied_rule = portal_rules[rule_id].\
Jérome Perrin's avatar
Jérome Perrin committed
720 721
              constructNewAppliedRule(portal_simulation,
                                      activate_kw=activate_kw)
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
              "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():
795 796
          sim_movement_list = m.getDeliveryRelatedValueList(
              portal_type='Simulation Movement') # XXX hardcoded
797 798 799 800 801 802 803 804 805
          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)
806 807

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

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

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

    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)

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 938
    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