SimulationMovement.py 23.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# 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.
#
##############################################################################

30
import zope.interface
31
from Products.ERP5Type.Globals import InitializeClass
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33 34
from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName

35
from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces
36
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
37
from Products.ERP5Type.Globals import PersistentMapping
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38

39
from Products.ERP5.Document.Movement import Movement
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40

41
from zLOG import LOG, WARNING
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42

43 44
from Acquisition import aq_base

45
from Products.ERP5.Document.AppliedRule import TREE_DELIVERED_CACHE_KEY, TREE_DELIVERED_CACHE_ENABLED
46
from Products.ERP5Type.patches.WorkflowTool import WorkflowHistoryList
47
from Products.ERP5.mixin.property_recordable import PropertyRecordableMixin
48

Jean-Paul Smets's avatar
Jean-Paul Smets committed
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
# XXX Do we need to create groups ? (ie. confirm group include confirmed, getting_ready and ready

parent_to_movement_simulation_state = {
  'cancelled'        : 'cancelled',
  'draft'            : 'draft',
  'auto_planned'     : 'auto_planned',
  'planned'          : 'planned',
  'ordered'          : 'planned',
  'confirmed'        : 'planned',
  'getting_ready'    : 'planned',
  'ready'            : 'planned',
  'started'          : 'planned',
  'stopped'          : 'planned',
  'delivered'        : 'planned',
  'invoiced'         : 'planned',
}

66
class SimulationMovement(Movement, PropertyRecordableMixin):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
  """
      Simulation movements belong to a simulation workflow which includes
      the following steps

      - planned

      - ordered

      - confirmed (the movement is now confirmed in qty or date)

      - started (the movement has started)

      - stopped (the movement is now finished)

      - delivered (the movement is now archived in a delivery)

      The simulation worklow uses some variables, which are
      set by the template

      - is_order_required

      - is_delivery_required


      XX
      - is_problem_checking_required ?

      Other flag
      (forzen flag)

      NEW: we do not use DCWorklow so that the simulation process
      can be as much as possible independent of a Zope / CMF implementation.
  """
  meta_type = 'ERP5 Simulation Movement'
  portal_type = 'Simulation Movement'

  # Declarative security
  security = ClassSecurityInfo()
105
  security.declareObjectProtected(Permissions.AccessContentsInformation)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
106 107 108 109 110 111 112 113 114 115

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.SimpleItem
                    , PropertySheet.CategoryCore
                    , PropertySheet.Amount
                    , PropertySheet.Task
                    , PropertySheet.Arrow
                    , PropertySheet.Movement
                    , PropertySheet.Simulation
116 117 118
                    # Need industrial_phase
                    , PropertySheet.TransformedResource
                    , PropertySheet.AppliedRule
119
                    , PropertySheet.ItemAggregation
120
                    , PropertySheet.Reference
Jean-Paul Smets's avatar
Jean-Paul Smets committed
121
                    )
122

123 124 125
  # Declarative interfaces
  zope.interface.implements(interfaces.IPropertyRecordable, )

126 127 128
  def tpValues(self) :
    """ show the content in the left pane of the ZMI """
    return self.objectValues()
129

Jean-Paul Smets's avatar
Jean-Paul Smets committed
130
  # Price should be acquired
131 132
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getPrice')
133
  def getPrice(self, default=None, context=None, REQUEST=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
134 135
    """
    """
136
    return self._baseGetPrice(default) # Call the price method
Jean-Paul Smets's avatar
Jean-Paul Smets committed
137

138 139
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getCausalityState')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
140 141 142 143
  def getCausalityState(self):
    """
      Returns the current state in causality
    """
144
    return getattr(aq_base(self), 'causality_state', 'solved')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
145

146 147
  security.declareProtected( Permissions.ModifyPortalContent,
                             'setCausalityState')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
148 149 150 151 152 153
  def setCausalityState(self, value):
    """
      Change causality state
    """
    self.causality_state = value

154 155
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getSimulationState')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
156 157 158 159
  def getSimulationState(self, id_only=1):
    """
      Returns the current state in simulation

160 161
      Inherit from order or delivery or parent (but use a conversion
      table to make orders planned when parent is confirmed)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
162 163 164 165 166 167 168 169 170 171

      XXX: movements in zero stock rule can not acquire simulation state
    """
    delivery = self.getDeliveryValue()
    if delivery is not None:
      return delivery.getSimulationState()
    order = self.getOrderValue()
    if order is not None:
      return order.getSimulationState()
    try:
172
      parent_state = self.getParentValue().getSimulationState()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
173
      return parent_to_movement_simulation_state[parent_state]
174
    except (KeyError, AttributeError):
175 176 177
      LOG('SimulationMovement.getSimulationState', WARNING,
          'Could not acquire simulation state from %s'
          % self.getRelativeUrl())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
178 179
      return None

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getTranslatedSimulationStateTitle')
  def getTranslatedSimulationStateTitle(self):
    """Returns translated simulation state title, for user interface, such as
    stock browser.
    """
    delivery = self.getDeliveryValue()
    if delivery is not None:
      return delivery.getTranslatedSimulationStateTitle()
    order = self.getOrderValue()
    if order is not None:
      return order.getTranslatedSimulationStateTitle()
    # The simulation_state of a simulation movement is calculated by a
    # mapping, there's no reliable way of getting the translated title from a
    # simulation state ID, so we just return the state ID because we got
    # nothing better to return.
    return self.getSimulationState()

198 199 200 201 202 203 204 205
  security.declareProtected(Permissions.AccessContentsInformation,
                            'isCompleted')
  def isCompleted(self):
    """Zope publisher docstring. Documentation in ISimulationMovement"""
    # only available in BPM, so fail totally in case of working without BPM
    return self.getSimulationState() in self.getCausalityValue(
        portal_type='Business Path').getCompletedStateList()

206 207
  security.declareProtected( Permissions.AccessContentsInformation,
                            'isAccountable')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
208 209 210
  def isAccountable(self):
    """
      Returns 1 if this needs to be accounted
211 212 213
      Some Simulation movement corresponds to non accountable movements,
      the parent applied rule decide wether this movement is accountable
      or not.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
214
    """
215
    return self.getParentValue().isAccountable(self)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
216 217

  # Ordering / Delivering
218 219
  security.declareProtected( Permissions.AccessContentsInformation,
                             'requiresOrder')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
220 221 222 223
  def requiresOrder(self):
    """
      Returns 1 if this needs to be ordered
    """
224
    if self.isOrderable():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
225 226 227 228
      return len(self.getCategoryMembership('order')) is 0
    else:
      return 0

229 230
  security.declareProtected( Permissions.AccessContentsInformation,
                             'requiresDelivery')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
231 232 233 234
  def requiresDelivery(self):
    """
      Returns 1 if this needs to be accounted
    """
235
    if self.isDeliverable():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
236 237 238 239 240 241 242 243 244
      return len(self.getCategoryMembership('delivery')) is 0
    else:
      return 0


  #######################################################
  # Causality Workflow Methods

  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
245
  def expand(self, force=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
246
    """
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
    Checks all existing applied rules and make sure they still apply.
    Checks for other possible rules and starts expansion process (instanciates
    applied rules and calls expand on them).

    First get all applicable rules,
    then, delete all applied rules that no longer match and are not linked to
    a delivery,
    finally, apply new rules if no rule with the same type is already applied.
    """
    portal_rules = getToolByName(self, 'portal_rules')

    tv = getTransactionalVariable(self)
    cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
    cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)

    # enable cache
    if not cache_enabled:
      cache[TREE_DELIVERED_CACHE_ENABLED] = 1

    applied_rule_dict = {}
    applicable_rule_dict = {}
    for rule in portal_rules.searchRuleList(self, sort_on='version',
        sort_order='descending'):
      ref = rule.getReference()
271
      if ref and ref not in applicable_rule_dict:
272 273
        applicable_rule_dict[ref] = rule

274
    for applied_rule in list(self.objectValues()):
275 276 277 278 279 280 281 282
      rule = applied_rule.getSpecialiseValue()
      if not applied_rule._isTreeDelivered() and not rule.test(self):
        self._delObject(applied_rule.getId())
      else:
        applied_rule_dict[rule.getPortalType()] = applied_rule

    for rule in applicable_rule_dict.itervalues():
      rule_type = rule.getPortalType()
283
      if rule_type not in applied_rule_dict:
284 285 286 287 288 289 290 291 292 293 294 295 296 297
        applied_rule = rule.constructNewAppliedRule(self, **kw)
        applied_rule_dict[rule_type] = applied_rule

    self.setCausalityState('expanded')
    # expand
    for applied_rule in applied_rule_dict.itervalues():
      applied_rule.expand(force=force, **kw)

    # disable and clear cache
    if not cache_enabled:
      try:
        del tv[TREE_DELIVERED_CACHE_KEY]
      except KeyError:
        pass
Jean-Paul Smets's avatar
Jean-Paul Smets committed
298 299 300 301 302 303 304 305 306 307

  security.declareProtected(Permissions.ModifyPortalContent, 'diverge')
  def diverge(self):
    """
       -> new status -> diverged

       Movements which diverge can not be expanded
    """
    self.setCausalityState('diverged')

308 309 310 311 312
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getExplanationValue')
  def getExplanationValue(self):
    """Returns the delivery if any or the order related to the root
    applied rule if any.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
313 314 315 316 317
    """
    if self.getDeliveryValue() is None:
      ra = self.getRootAppliedRule()
      order = ra.getCausalityValue()
      if order is not None:
318
        return order
Jean-Paul Smets's avatar
Jean-Paul Smets committed
319 320
      else:
        # Ex. zero stock rule
321
        return ra
Jean-Paul Smets's avatar
Jean-Paul Smets committed
322
    else:
323
      explanation_value = self.getDeliveryValue()
324 325
      while explanation_value.getPortalType() not in \
              self.getPortalDeliveryTypeList() and \
326
          explanation_value != self.getPortalObject():
327
            explanation_value = explanation_value.getParentValue()
328
      if explanation_value != self.getPortalObject():
329
        return explanation_value
330

Jean-Paul Smets's avatar
Jean-Paul Smets committed
331
  # Deliverability / orderability
332 333
  security.declareProtected( Permissions.AccessContentsInformation,
                             'isOrderable')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
334
  def isOrderable(self):
335
    applied_rule = self.getParentValue()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
336 337
    rule = applied_rule.getSpecialiseValue()
    if rule is not None:
338
      return rule.isOrderable(self)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
339 340
    return 0

341 342
  getOrderable = isOrderable

343 344
  security.declareProtected( Permissions.AccessContentsInformation,
                             'isDeliverable')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
345
  def isDeliverable(self):
346
    applied_rule = self.getParentValue()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
347 348
    rule = applied_rule.getSpecialiseValue()
    if rule is not None:
349
      return rule.isDeliverable(self)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
350 351
    return 0

352
  getDeliverable = isDeliverable
353

354
  # Simulation Dates - acquire target dates
355 356
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getOrderStartDate')
357 358 359 360
  def getOrderStartDate(self):
    order_value = self.getOrderValue()
    if order_value is not None:
      return order_value.getStartDate()
361

362 363
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getOrderStopDate')
364 365 366 367
  def getOrderStopDate(self):
    order_value = self.getOrderValue()
    if order_value is not None:
      return order_value.getStopDate()
Romain Courteaud's avatar
Romain Courteaud committed
368

369 370
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getDeliveryStartDateList')
Romain Courteaud's avatar
Romain Courteaud committed
371 372
  def getDeliveryStartDateList(self):
    """
373
      Returns the stop date of related delivery
Romain Courteaud's avatar
Romain Courteaud committed
374 375 376 377 378 379
    """
    start_date_list = []
    delivery_movement = self.getDeliveryValue()
    if delivery_movement is not None:
      start_date_list.append(delivery_movement.getStartDate())
    return start_date_list
380

381 382
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getDeliveryStopDateList')
Romain Courteaud's avatar
Romain Courteaud committed
383 384
  def getDeliveryStopDateList(self):
    """
385
      Returns the stop date of related delivery
Romain Courteaud's avatar
Romain Courteaud committed
386 387 388 389 390 391
    """
    stop_date_list = []
    delivery_movement = self.getDeliveryValue()
    if delivery_movement is not None:
      stop_date_list.append(delivery_movement.getStopDate())
    return stop_date_list
392

393 394
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getDeliveryQuantity')
Romain Courteaud's avatar
Romain Courteaud committed
395 396
  def getDeliveryQuantity(self):
    """
397
      Returns the quantity of related delivery
Romain Courteaud's avatar
Romain Courteaud committed
398
    """
399
    quantity = 0.0
Romain Courteaud's avatar
Romain Courteaud committed
400 401 402 403
    delivery_movement = self.getDeliveryValue()
    if delivery_movement is not None:
      quantity = delivery_movement.getQuantity()
    return quantity
404

405 406
  security.declareProtected( Permissions.AccessContentsInformation,
                             'isConvergent')
407 408
  def isConvergent(self):
    """
409
      Returns true if the Simulation Movement is convergent with the
410
      the delivery value
411 412 413
    """
    return not self.isDivergent()

414
  security.declareProtected( Permissions.AccessContentsInformation,
415
      'isDivergent')
416 417
  def isDivergent(self):
    """
418
      Returns true if the Simulation Movement is divergent from the
419
      the delivery value
420
    """
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
    return self.getParentValue().isDivergent(self)

  security.declareProtected( Permissions.AccessContentsInformation,
      'getDivergenceList')
  def getDivergenceList(self):
    """
    Returns detailed information about the divergence
    """
    return self.getParentValue().getDivergenceList(self)

  security.declareProtected( Permissions.AccessContentsInformation,
      'getSolverList')
  def getSolverList(self):
    """
    Returns solvers that can fix the current divergence
    """
    return self.getParentValue().getSolverList(self)

439 440
  security.declareProtected( Permissions.ModifyPortalContent,
                             'setDefaultDeliveryProperties')
441 442
  def setDefaultDeliveryProperties(self):
    """
443 444
    Sets the delivery_ratio and delivery_error properties to the
    calculated value
445 446 447 448 449
    """
    delivery = self.getDeliveryValue()
    if delivery is not None:
      delivery.updateSimulationDeliveryProperties(movement_list = [self])

450 451
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getCorrectedQuantity')
452 453
  def getCorrectedQuantity(self):
    """
454 455
    Returns the quantity property deducted by the possible profit_quantity and
    taking into account delivery error
456 457 458 459 460

    NOTE: XXX-JPS This method should not use profit_quantity. Profit and loss
          quantities are now only handled through explicit movements.
          Look are invocations of _isProfitAndLossMovement in
          ERP5.mixin.rule to understand how.
461 462
    """
    quantity = self.getQuantity()
463 464 465
    profit_quantity = self.getProfitQuantity() or 0
    delivery_error = self.getDeliveryError() or 0
    return quantity - profit_quantity + delivery_error
466

467 468
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getRootSimulationMovement')
469 470 471
  def getRootSimulationMovement(self):
    """
      Return the root simulation movement in the simulation tree.
472
      FIXME : this method should be called getRootSimulationMovementValue
473
    """
474
    parent_applied_rule = self.getParentValue()
475 476 477 478 479
    if parent_applied_rule.getRootAppliedRule() == parent_applied_rule:
      return self
    else:
      return parent_applied_rule.getRootSimulationMovement()

480 481
  security.declareProtected( Permissions.AccessContentsInformation,
                             'getRootSimulationMovementUid')
482 483 484 485 486 487 488 489 490
  def getRootSimulationMovementUid(self):
    """
      Return the uid of the root simulation movement in the simulation tree.
    """
    root_simulation_movement = self.getRootSimulationMovement()
    if root_simulation_movement is not None:
      return root_simulation_movement.getUid()
    return None

491
  security.declareProtected( Permissions.AccessContentsInformation,
492
                             'getRootCausalityValueList')
493 494 495 496 497 498 499 500
  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
    """
    root_rule = self.getRootAppliedRule()
    return root_rule.getCausalityValueList()
501

502
  # XXX FIXME Use a interaction workflow instead
503
  # XXX This behavior is now done by simulation_movement_interaction_workflow
504
  # The call to activate() must be done after actual call to
505
  # setDelivery() on the movement,
506
  # but activate() must be called on the previous delivery...
507 508 509 510 511 512 513 514
  #def _setDelivery(self, value):
  #  LOG('setDelivery before', 0, '')
  #  delivery_value = self.getDeliveryValue()
  #  Movement.setDelivery(value)
  #  LOG('setDelivery', 0, '')
  #  if delivery_value is not None:
  #    LOG('delivery_value = ', 0, repr(delivery_value))
  #    activity = delivery_value.activate(
515
  #                activity='SQLQueue',
516
  #                after_path_and_method_id=(
517 518
  #                                        self.getPath(),
  #                                        ['immediateReindexObject',
519 520
  #                                         'recursiveImmediateReindexObject']))
  #    activity.edit()
521

522 523 524 525 526 527 528 529 530 531 532 533 534
  def _isTreeDelivered(self, ignore_first=0):
    """
    checks if subapplied rules  of this movement (going down the complete
    simulation tree) have a child with a delivery relation.
    Returns True if at least one is delivered, False if none of them are.

    see AppliedRule._isTreeDelivered
    """
    tv = getTransactionalVariable(self)
    cache = tv.setdefault(TREE_DELIVERED_CACHE_KEY, {})
    cache_enabled = cache.get(TREE_DELIVERED_CACHE_ENABLED, 0)

    def getTreeDelivered(movement, ignore_first=0):
535
      if not ignore_first:
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
        if len(movement.getDeliveryList()) > 0:
          return True
      for applied_rule in movement.objectValues():
        if applied_rule._isTreeDelivered():
          return True
      return False

    if ignore_first:
      rule_key = (self.getRelativeUrl(), 1)
    else:
      rule_key = self.getRelativeUrl()
    if cache_enabled:
      try:
        return cache[rule_key]
      except:
        result = getTreeDelivered(self, ignore_first=ignore_first)
        cache[rule_key] = result
        return result
    else:
      return getTreeDelivered(self, ignore_first=ignore_first)

557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575
  security.declareProtected(Permissions.AccessContentsInformation,
                            'isBuildable')
  def isBuildable(self):
    """Simulation Movement buildable logic"""
    if self.getDeliveryValue() is not None:
      # already delivered
      return False
    # might be buildable - business path depended
    business_path = self.getCausalityValue(portal_type='Business Path')
    explanation_value = self.getExplanationValue()
    if business_path is not None and explanation_value is not None:
      predecessor = business_path.getPredecessorValue()
      if predecessor is None:
        # first one, can be built
        return True
      else:
        for successor_related in predecessor.getSuccessorRelatedValueList():
          for business_path_movement in successor_related \
              .getRelatedSimulationMovementValueList(explanation_value):
Łukasz Nowak's avatar
Łukasz Nowak committed
576 577
            if successor_related.isMovementRelatedWithMovement(self,
                business_path_movement):
578 579 580 581 582 583 584 585 586 587 588 589 590
              business_path_movement_delivery = business_path_movement \
                  .getDeliveryValue()
              if business_path_movement_delivery is None:
                return False # related movement is not delivered yet
              business_path_movement_delivery_document = \
                  business_path_movement_delivery.getParentValue()
              # here we can optimise somehow, as
              # business_path_movement_delivery_document would repeat
              if not successor_related.isCompleted(
                  business_path_movement_delivery_document):
                # related movements delivery is not completed
                return False
    return True
591 592 593 594 595 596

  security.declareProtected( Permissions.ModifyPortalContent,
                             'appendDecision')
  def appendDecision(self, decision):
    """Appends decision, optionally initialises"""
    property = decision.divergence.tested_property
597
    if getattr(aq_base(self), 'divergence_solution_history', None) is None:
598
      # initialise divergence history mapping
599 600 601 602
      self.divergence_solution_history = PersistentMapping()
    if self.divergence_solution_history.get(property, None) is None:
      self.divergence_solution_history[property] = WorkflowHistoryList()
    self.divergence_solution_history[property].append(decision)
603 604 605 606 607

  security.declareProtected( Permissions.AccessContentsInformation,
                             'isPropertyForced')
  def isPropertyForced(self, property):
    """Check if property was forced by user"""
608 609 610
    divergence_solution_history = getattr(aq_base(self),
        'divergence_solution_history', None)
    if divergence_solution_history is None:
611 612
      return False

613
    for decision in divergence_solution_history.get(property, [])[::-1]:
614 615 616 617 618 619 620 621 622
      # fuzzy logic:
      #  * if there was accept decision with force - force
      #  * but if there was accept without force after - do not force
      # To be discussed.
      if decision.decision == 'accept':
        if decision.force_property:
          return True
        return False
    return False
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661

  def getSolverProcessValueList(self, movement=None, validation_state=None):
    """
    Returns the list of solver processes which are
    are in a given state and which apply to delivery_or_movement.
    This method is useful to find applicable solver processes
    for a delivery.

    movement -- not applicable

    validation_state -- a state of a list of states
                        to filter the result
    """
    raise NotImplementedError

  def getSolverDecisionValueList(self, movement=None, validation_state=None):
    """
    Returns the list of solver decisions which apply
    to a given movement.

    movement -- not applicable

    validation_state -- a state of a list of states
                        to filter the result
    """
    raise NotImplementedError

  def getSolvedPropertyApplicationValueList(self, movement=None, divergence_tester=None):
    """
    Returns the list of documents at which a given divergence resolution
    can be resolved at. For example, in most cases, date divergences can
    only be resolved at delivery level whereas quantities are usually
    resolved at cell level.

    The result of this method is a list of ERP5 documents.

    movement -- not applicable
    """
    raise NotImplementedError