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

31 32
import zope.interface

Jean-Paul Smets's avatar
Jean-Paul Smets committed
33 34
from Products.CMFCore.utils import getToolByName
from AccessControl import ClassSecurityInfo
35
from Products.ERP5Type import Permissions, PropertySheet, interfaces
36
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from Products.ERP5Type.XMLObject import XMLObject
38
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
39
from Products.ERP5.mixin.composition import CompositionMixin
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, CompositionMixin):
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, base_contribution=None, rounding=False, **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.
124 125

        base_contribution must be a relative url of a category.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
126
      """
127 128
      result = None
      if not fast:
129 130
        kw.setdefault( 'portal_type',
                       self.getPortalDeliveryMovementTypeList())
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
        if base_contribution is None:
          result = sum([ line.getTotalPrice(fast=0) for line in
                         self.objectValues(**kw) ])
        else:
          # Find amounts from movements in the delivery.
          if isinstance(base_contribution, (tuple, list)):
            base_contribution_list = base_contribution
          else:
            base_contribution_list = (base_contribution,)
          base_contribution_value_list = []
          portal_categories = self.portal_categories
          for relative_url in base_contribution_list:
            base_contribution_value = portal_categories.getCategoryValue(relative_url)
            if base_contribution_value is not None:
              base_contribution_value_list.append(base_contribution_value)
          if not base_contribution_value_list:
            # We cannot find any amount so that the result is 0.
            result = 0
          else:
            matched_movement_list = [
                movement
                for movement in self.getMovementList()
                if set(movement.getBaseContributionValueList()).intersection(base_contribution_value_list)]
            if rounding:
              portal_roundings = self.portal_roundings
              matched_movement_list = [
                  portal_roundings.getRoundingProxy(movement)
                  for movement in matched_movement_list]
            result = sum([movement.getTotalPrice()
                          for movement in matched_movement_list])
161 162 163 164 165 166 167
      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
168
      method = self._getTypeBasedMethod('convertTotalPrice')
169
      if method is not None:
170 171
        return method(result)
      return result
Jean-Paul Smets's avatar
Jean-Paul Smets committed
172

173 174 175 176 177 178 179 180 181 182
    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)

183
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
184
                              'getTotalQuantity')
185
    def getTotalQuantity(self, fast=0, src__=0, **kw):
186 187 188 189
      """ 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
190 191 192

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

Jérome Perrin's avatar
Jérome Perrin committed
206 207
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryUid')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
208 209 210
    def getDeliveryUid(self):
      return self.getUid()

Jérome Perrin's avatar
Jérome Perrin committed
211 212
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDeliveryValue')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
213
    def getDeliveryValue(self):
214 215 216 217 218
      """
      Deprecated, we should use getRootDeliveryValue instead
      """
      return self

Jérome Perrin's avatar
Jérome Perrin committed
219 220
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getRootDeliveryValue')
221 222 223 224 225
    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
226 227
      return self

Jérome Perrin's avatar
Jérome Perrin committed
228 229
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getDelivery')
230 231 232
    def getDelivery(self):
      return self.getRelativeUrl()

233
    security.declareProtected(Permissions.AccessContentsInformation,
234 235
                             '_getMovementList')
    def _getMovementList(self, portal_type=None, **kw):
236 237
      """
        Return a list of movements.
238 239
        First, we collect movements by movement type portal types, then
        we filter the result by specified portal types.
240
      """
241
      movement_portal_type_list = self.getPortalMovementTypeList()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
242
      movement_list = []
243
      add_movement = movement_list.append
244
      extend_movement = movement_list.extend
245
      sub_object_list = self.objectValues(portal_type=movement_portal_type_list)
246 247 248 249
      extend_sub_object = sub_object_list.extend
      append_sub_object = sub_object_list.append
      while sub_object_list:
        sub_object = sub_object_list.pop()
250
        content_list = sub_object.objectValues(portal_type=movement_portal_type_list)
251 252 253 254 255 256 257 258 259 260
        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
261
        else:
262
          add_movement(sub_object)
263 264 265 266 267 268
      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
269
      return movement_list
270 271 272 273 274 275 276 277
    
    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
278

Jérome Perrin's avatar
Jérome Perrin committed
279 280
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getSimulatedMovementList')
281 282 283 284 285
    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
286 287
      return self.getMovementList(portal_type=
                          self.getPortalSimulatedMovementTypeList())
288

Jérome Perrin's avatar
Jérome Perrin committed
289 290
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getInvoiceMovementList')
291 292 293 294 295
    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
296 297
      return self.getMovementList(portal_type=
                            self.getPortalInvoiceMovementTypeList())
298

Jérome Perrin's avatar
Jérome Perrin committed
299 300
    security.declareProtected(Permissions.AccessContentsInformation,
                              'getContainerList')
301 302 303 304 305 306
    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
307 308
      for m in self.contentValues(filter={'portal_type':
                                  self.getPortalContainerTypeList()}):
309 310 311
        container_list.append(m)
      return container_list

312
    def applyToDeliveryRelatedMovement(self, portal_type='Simulation Movement',
313
                                       method_id='expand', **kw):
314
      for simulation_movement in self._getAllRelatedSimulationMovementList():
315
        # And apply
316
        getattr(simulation_movement.getObject(), method_id)(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
317 318 319 320

    #######################################################
    # Causality computation
    security.declareProtected(Permissions.View, 'isConvergent')
321
    def isConvergent(self,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
322 323 324
      """
        Returns 0 if the target is not met
      """
325
      return int(not self.isDivergent(**kw))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
326

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

344
    security.declareProtected(Permissions.View, 'isDivergent')
345
    def isDivergent(self, fast=0, **kw):
346 347 348 349
      """
        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
350

351 352
        emit targetUnreachable !
      """
353 354
      ## Note that fast option was removed. Now, fast=1 is ignored.
      
355
      # Check if the total quantity equals the total of each simulation movement quantity
356 357 358
      for movement in self.getMovementList():
        if movement.isDivergent():
          return 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
359 360
      return 0

361
    security.declareProtected(Permissions.View, 'getDivergenceList')
362
    def getDivergenceList(self, **kw):
363 364 365 366 367 368 369 370
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
      for movement in self.getMovementList():
         divergence_list.extend(movement.getDivergenceList())
      return divergence_list

371
    @UnrestrictedMethod
372
    def updateCausalityState(self, **kw):
373 374 375 376 377
      """
      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
      """
378 379
      if getattr(self, 'diverge', None) is not None \
            and getattr(self, 'converge', None) is not None:
380
        if self.isDivergent(**kw):
381 382 383
          self.diverge()
        else:
          self.converge()
384

385
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
386 387
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
      """
      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
406
      if start_date is not None or stop_date is not None:
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
        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:
422 423
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
      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
460 461 462 463 464 465
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
466
      self.recursiveReindexObject(*k, **kw)
467 468 469
      # 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
470 471 472 473 474

    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
      resource_dict = {}
Romain Courteaud's avatar
Romain Courteaud committed
475 476
      for m in self.contentValues(filter={
                      'portal_type': self.getPortalMovementTypeList()}):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
477 478 479 480 481
        r = m.getResource()
        if r is not None:
          resource_dict[r] = 1
      return resource_dict.keys()

482
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
483
                              'getInventory')
484 485 486 487 488 489
    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
490

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

500
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
501
                              'getAvailableInventory')
502
    def getAvailableInventory(self, **kw):
503 504 505 506
      """
      Returns available inventory
      (current inventory - deliverable)
      """
Romain Courteaud's avatar
Romain Courteaud committed
507
      kw['resource'] = self._getMovementResourceList()
508 509
      return self.portal_simulation.getAvailableInventory(**kw)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Romain Courteaud's avatar
Romain Courteaud committed
636 637 638 639




640 641 642 643 644 645 646 647 648
# 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)
649
#
650 651 652 653 654 655 656
#     security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice')
#     def getFutureInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getFutureInventoryAssetPrice(**kw)
657
#
658 659 660 661 662 663 664
#     security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice')
#     def getCurrentInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getCurrentInventoryAssetPrice(**kw)
665
#
666 667 668 669 670 671 672 673
#     security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice')
#     def getAvailableInventoryAssetPrice(self, **kw):
#       """
#         Returns asset at infinite
#       """
#       kw['category'] = self._getMovementResourceList()
#       return self.portal_simulation.getAvailableInventoryAssetPrice(**kw)

674 675 676 677 678 679 680 681 682 683 684
    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

685 686
    ##########################################################################
    # Applied Rule stuff
687 688 689
    @UnrestrictedMethod
    def updateAppliedRule(self, rule_reference=None, rule_id=None, force=0,
                          **kw):
690
      """
691
      Create a new Applied Rule if none is related, or call expand
692
      on the existing one.
693 694

      The chosen applied rule will be the validated rule with reference ==
695
      rule_reference, and the higher version number.
696
      """
697 698 699 700 701 702
      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

703
      if rule_reference is None:
704
        return
705 706 707 708 709 710

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

711
      portal_rules = getToolByName(self, 'portal_rules')
712
      res = portal_rules.searchFolder(reference=rule_reference,
713 714 715 716 717
          validation_state="validated", sort_on='version',
          sort_order='descending') # XXX validated is Hardcoded !

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

721
      self._createAppliedRule(rule_id, force=force, **kw)
722

723
    def _createAppliedRule(self, rule_id, force=0, activate_kw=None, **kw):
724 725 726 727 728
      """
        Create a new Applied Rule is none is related, or call expand
        on the existing one.
      """
      # Look up if existing applied rule
729 730
      my_applied_rule_list = self.getCausalityRelatedValueList(
          portal_type='Applied Rule')
731
      my_applied_rule = None
732
      if len(my_applied_rule_list) == 0:
733 734
        if self.isSimulated():
          # No need to create a DeliveryRule
735
          # if we are already in the simulation process
736 737 738 739 740 741
          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
742 743
              constructNewAppliedRule(portal_simulation,
                                      activate_kw=activate_kw)
744 745 746 747
          # Set causality
          my_applied_rule.setCausalityValue(self)
          # We must make sure this rule is indexed
          # now in order not to create another one later
748
          my_applied_rule.reindexObject(activate_kw=activate_kw, **kw)
749 750 751 752
      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
753
        raise "SimulationError", 'Delivery %s has more than one applied'\
754
            ' rule.' % self.getRelativeUrl()
755

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

    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
770 771
    @UnrestrictedMethod
    def expand(self, applied_rule_id=None, force=0, activate_kw=None,**kw):
772 773
      """
        Reexpand applied rule
774

775 776 777 778 779 780 781 782 783 784 785 786 787
        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:
788
          LOG("ERP5", PROBLEM,
789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
              "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 = []
814 815 816 817 818 819
      for sim_movement in self._getAllRelatedSimulationMovementList():
        if sim_movement.getRootAppliedRule().getPath() \
            not in excluded_rule_path_list:
          parent_value = sim_movement.getParentValue()
          if parent_value not in to_expand_list:
            to_expand_list.append(parent_value)
820 821 822
      for rule in to_expand_list:
        rule.expand(activate_kw=activate_kw,**kw)
        rule.recursiveReindexObject(activate_kw=activate_kw)
823 824

    security.declareProtected( Permissions.AccessContentsInformation,
825
                               'getRootCausalityValueList')
826 827 828 829 830 831
    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
      """
832 833
      causality_value_list = [x for x in self.getCausalityValueList()
                                if x is not self]
834 835 836 837 838
      initial_list = []
      if len(causality_value_list)==0:
        initial_list = [self]
      else:
        for causality in causality_value_list:
839 840 841 842
          # The causality may be something which has not this method
          # (e.g. item)
          if hasattr(causality, 'getRootCausalityValueList'):
            tmp_causality_list = causality.getRootCausalityValueList()
843
            initial_list.extend([x for x in tmp_causality_list
844
                                 if x not in initial_list])
845 846 847 848 849 850 851
      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,
852
                               'setRootCausalityValueList')
853
    def setRootCausalityValueList(self,value):
854
      """
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875
      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
876 877 878
      """
      pass

879 880 881
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
882 883 884 885 886
      # 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
887 888 889 890 891 892 893 894

    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.' \
895
              % self.getPortalType().replace(' ', '')
896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911

    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)

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 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
    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
956 957 958 959 960 961 962 963 964 965 966 967

    def _getAllRelatedSimulationMovementList(self, **kw):
      search_method = \
          self.getPortalObject().portal_catalog.unrestrictedSearchResults
      movement_uid_list = [x.getUid() for x in self.getMovementList()]
      sim_movement_list = search_method(portal_type='Simulation Movement',
                                        delivery_uid=movement_uid_list, **kw)
      if len(sim_movement_list) == 0:
        # 'order' category is deprecated. it is kept for compatibility.
        sim_movement_list = search_method(portal_type='Simulation Movement',
                                          order_uid=movement_uid_list, **kw)
      return sim_movement_list