Delivery.py 40.2 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
      for simulation_movement in self._getAllRelatedSimulationMovementList():
        if simulation_movement.isDivergent():
358
          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
      """
      Return a list of messages that contains the divergences
      """
      divergence_list = []
367 368
      for simulation_movement in self._getAllRelatedSimulationMovementList():
         divergence_list.extend(simulation_movement.getDivergenceList())
369 370
      return divergence_list

371
    @UnrestrictedMethod
372
    def updateCausalityState(self, solve_automatically=True, **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 380 381
      isTransitionPossible = \
          self.getPortalObject().portal_workflow.isTransitionPossible
      if isTransitionPossible(self, 'diverge') and \
          isTransitionPossible(self, 'converge'):
382
        if self.isDivergent(**kw):
383 384 385 386
          # If delivery is not simulated (PackingList.isDivergent()
          # returns True in such a case), we cannot solve divergence
          # anyway.
          if self.isSimulated() and solve_automatically and \
387 388 389 390
              isTransitionPossible(self, 'solve_automatically'):
            self.solveAutomatically()
          else:
            self.diverge()
391 392
        else:
          self.converge()
393

394
    def splitAndDeferMovementList(self, start_date=None, stop_date=None,
395 396
        movement_uid_list=[], delivery_solver=None,
        target_solver='CopyToTarget', delivery_builder=None):
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
      """
      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
415
      if start_date is not None or stop_date is not None:
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
        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:
431 432
        movement.activate(tag=solver_tag).Movement_solveMovement(
            delivery_solver, target_solver)
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 460 461 462 463 464 465 466 467 468
      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)


469 470 471 472 473 474 475 476 477 478 479
    #######################################################
    # Defer indexing process
    def reindexObject(self, *k, **kw):
      """
        Reindex children and simulation
      """
      self.recursiveReindexObject(*k, **kw)
      # 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
480 481 482 483
    #######################################################
    # Stock Management
    def _getMovementResourceList(self):
      resource_dict = {}
Romain Courteaud's avatar
Romain Courteaud committed
484 485
      for m in self.contentValues(filter={
                      'portal_type': self.getPortalMovementTypeList()}):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
486 487 488 489 490
        r = m.getResource()
        if r is not None:
          resource_dict[r] = 1
      return resource_dict.keys()

491
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
492
                              'getInventory')
493 494 495 496 497 498
    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
499

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

509
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
510
                              'getAvailableInventory')
511
    def getAvailableInventory(self, **kw):
512 513 514 515
      """
      Returns available inventory
      (current inventory - deliverable)
      """
Romain Courteaud's avatar
Romain Courteaud committed
516
      kw['resource'] = self._getMovementResourceList()
517 518
      return self.portal_simulation.getAvailableInventory(**kw)

519
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
520
                              'getFutureInventory')
521
    def getFutureInventory(self, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
522
      """
523
      Returns inventory at infinite
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.getFutureInventory(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
527

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

537
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
538
                              'getCurrentInventoryList')
539
    def getCurrentInventoryList(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.getCurrentInventoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
545

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

555
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
556
                              'getInventoryStat')
557
    def getInventoryStat(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.getInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
563

564
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
565
                              'getCurrentInventoryStat')
566
    def getCurrentInventoryStat(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.getCurrentInventoryStat(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
572

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

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

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

600
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
601
                              'getFutureInventoryChart')
602
    def getFutureInventoryChart(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.getFutureInventoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
608

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

618
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
619
                              'getInventoryHistoryChart')
620
    def getInventoryHistoryChart(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.getInventoryHistoryChart(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
626

627
    security.declareProtected(Permissions.AccessContentsInformation,
Romain Courteaud's avatar
Romain Courteaud committed
628
                              'getMovementHistoryList')
629
    def getMovementHistoryList(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.getMovementHistoryList(**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
635

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

Romain Courteaud's avatar
Romain Courteaud committed
645 646 647 648




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

683 684 685 686 687 688 689 690 691 692 693
    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

694 695
    ##########################################################################
    # Applied Rule stuff
696 697 698
    @UnrestrictedMethod
    def updateAppliedRule(self, rule_reference=None, rule_id=None, force=0,
                          **kw):
699
      """
700
      Create a new Applied Rule if none is related, or call expand
701
      on the existing one.
702 703

      The chosen applied rule will be the validated rule with reference ==
704
      rule_reference, and the higher version number.
705
      """
706 707 708 709 710 711
      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

712
      if rule_reference is None:
713
        return
714 715 716

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

720
      portal_rules = getToolByName(self, 'portal_rules')
721
      res = portal_rules.searchFolder(reference=rule_reference,
722 723 724 725 726
          validation_state="validated", sort_on='version',
          sort_order='descending') # XXX validated is Hardcoded !

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

730
      self._createAppliedRule(rule_id, force=force, **kw)
731

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

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

    security.declareProtected(Permissions.ModifyPortalContent, 'expand')
779 780
    @UnrestrictedMethod
    def expand(self, applied_rule_id=None, force=0, activate_kw=None,**kw):
781 782
      """
        Reexpand applied rule
783

784 785 786 787 788 789 790 791 792 793 794 795 796
        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:
797
          LOG("ERP5", PROBLEM,
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
              "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 = []
823 824 825 826 827 828
      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)
829 830 831
      for rule in to_expand_list:
        rule.expand(activate_kw=activate_kw,**kw)
        rule.recursiveReindexObject(activate_kw=activate_kw)
832 833

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

888 889 890
    def getBuilderList(self):
      """Returns appropriate builder list."""
      return self._getTypeBasedMethod('getBuilderList')()
891 892 893 894 895
      # 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
896 897 898 899 900 901 902 903

    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.' \
904
              % self.getPortalType().replace(' ', '')
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920

    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)

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 956 957 958 959 960 961 962 963 964
    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
965 966 967 968 969

    def _getAllRelatedSimulationMovementList(self, **kw):
      search_method = \
          self.getPortalObject().portal_catalog.unrestrictedSearchResults
      movement_uid_list = [x.getUid() for x in self.getMovementList()]
970 971
      if len(movement_uid_list) == 0:
        return []
972 973 974
      sim_movement_list = search_method(portal_type='Simulation Movement',
                                        delivery_uid=movement_uid_list, **kw)
      return sim_movement_list
975 976 977 978 979 980 981 982 983 984 985 986

    def getDivergentTesterAndSimulationMovementList(self):
      """
      This method returns a list of (tester, simulation_movement) for each divergence.
      """
      divergent_tester_list = []
      for simulation_movement in self._getAllRelatedSimulationMovementList():
        rule = simulation_movement.getParentValue().getSpecialiseValue()
        for tester in rule._getDivergenceTesterList(exclude_quantity=False):
          if tester.explain(simulation_movement) not in (None, []):
            divergent_tester_list.append((tester, simulation_movement))
      return divergent_tester_list