BPMRule.py 15.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
#                    Łukasz Nowak <luke@nexedi.com>
#
# 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.
#
##############################################################################

import zope.interface
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, interfaces
34 35 36 37
from Products.ERP5.Document.Predicate import Predicate
from Products.ERP5Type.XMLObject import XMLObject
from Acquisition import aq_base
from Products.CMFCore.utils import getToolByName
38

39
class BPMRule(Predicate, XMLObject):
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  """
    DISCLAIMER: Do not use this in any production system.
                This is only proof of concept and evaluation of new system
                design. Implementation and API can change without any
                further warning.

                *DO NOT USE IN PRODUCTION SYSTEM*

    This is BPM enabled Rule system Base class.
  """

  # CMF Type Definition
  meta_type = 'ERP5 BPM Rule'
  portal_type = 'BPM Rule'
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1
  isPredicate = 1

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  zope.interface.implements( interfaces.IPredicate,
                     interfaces.IRule )

  # Default Properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.CategoryCore
                    , PropertySheet.DublinCore
                    , PropertySheet.Task
                    , PropertySheet.Predicate
                    , PropertySheet.Reference
                    , PropertySheet.Version
75
                    , PropertySheet.AppliedRule
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 105 106 107 108 109 110 111 112 113 114 115 116 117
  movement_type = 'Simulation Movement'

  security.declareProtected(Permissions.AccessContentsInformation,
                            'isAccountable')
  def isAccountable(self, movement):
    """Tells wether generated movement needs to be accounted or not.

    Only account movements which are not associated to a delivery;
    Whenever delivery is there, delivery has priority
    """
    return movement.getDeliveryValue() is None

  security.declareProtected(Permissions.ModifyPortalContent,
                            'constructNewAppliedRule')
  def constructNewAppliedRule(self, context, id=None, activate_kw=None):
    """
      Creates a new applied rule which points to self
    """
    portal_types = getToolByName(self, 'portal_types')
    if id is None:
      id = context.generateNewId()
    if getattr(aq_base(context), id, None) is None:
      context.newContent(id=id,
                         portal_type='Applied Rule',
                         specialise_value=self,
                         activate_kw=activate_kw)
    return context.get(id)

  # Simulation workflow
  def test(self, *args, **kw):
    """
    If no test method is defined, return False, to prevent infinite loop
    """
    if not self.getTestMethodId():
      return False
    return Predicate.test(self, *args, **kw)

  # Solvers
  security.declareProtected( Permissions.AccessContentsInformation,
                            'isDivergent')
118
  def isDivergent(self, simulation_movement, ignore_list=[]):
119 120 121 122
    """
    Returns true if the Simulation Movement is divergent comparing to
    the delivery value
    """
123
    delivery = simulation_movement.getDeliveryValue()
124 125 126
    if delivery is None:
      return 0

127
    if self.getDivergenceList(simulation_movement) == []:
128 129 130 131 132
      return 0
    else:
      return 1

  security.declareProtected(Permissions.View, 'getDivergenceList')
133
  def getDivergenceList(self, simulation_movement):
134 135 136 137 138 139
    """
    Return a list of messages that contains the divergences.
    """
    result_list = []
    for divergence_tester in self.contentValues(
               portal_type=self.getPortalDivergenceTesterTypeList()):
140
      result = divergence_tester.explain(simulation_movement)
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
      result_list.extend(result)
    return result_list

  # Deliverability / orderability
  def isOrderable(self, movement):
    return 0

  def isDeliverable(self, movement):
    return 0

  def isStable(self, applied_rule, **kw):
    """
    - generate a list of previsions
    - compare the prevision with existing children
    - return 1 if they match, 0 else
    """
    list = self._getCompensatedMovementList(applied_rule, **kw)
    for e in list:
      if len(e) > 0:
        return 0
    return 1

#### Helpers to overload
164 165
  def _getExpandablePropertyUpdateDict(self, applied_rule, movement,
      business_path, current_property_dict):
166 167 168 169 170 171 172 173 174 175
    """Rule specific dictionary used to update _getExpandablePropertyDict
    This method might be overloaded.
    """
    return {}

  def _getInputMovementList(self, applied_rule):
    """Return list of movements for applied rule.
    This method might be overloaded"""
    return [applied_rule.getParentValue()]

176 177 178 179 180 181 182 183
  def _generatePrevisionList(self, applied_rule, **kw):
    """
    Generate a list of dictionaries, that contain calculated content of
    current Simulation Movements in applied rule.
    based on its context (parent movement, delivery, configuration ...)

    These previsions are returned as dictionaries.
    """
184
    prevision_dict_list = []
185 186
    for input_movement, business_path in self \
        ._getInputMovementAndPathTupleList(applied_rule):
187 188 189 190 191
      prevision_dict_list.append(self._getExpandablePropertyDict(applied_rule,
          input_movement, business_path))
    return prevision_dict_list

#### Helpers NOT to overload
192 193 194
  def _getCurrentMovementList(self, applied_rule, **kw):
    """
    Returns the list of current children of the applied rule, sorted in 3
Łukasz Nowak's avatar
Łukasz Nowak committed
195
    groups : immutable/mutable/deletable
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219

     * immutable is frozen
     * mutable is not frozen, but delivered
     * deletable is not frozen and not delivered

    Delivered means movement is delivered or any of its children is delivered.
    """
    immutable_movement_list = []
    mutable_movement_list = []
    deletable_movement_list = []

    for movement in applied_rule.contentValues(
        portal_type=self.movement_type):
      if movement.isFrozen():
        immutable_movement_list.append(movement)
      else:
        if movement._isTreeDelivered():
          mutable_movement_list.append(movement)
        else:
          deletable_movement_list.append(movement)

    return (immutable_movement_list, mutable_movement_list,
            deletable_movement_list)

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
  def _getInputMovementAndPathTupleList(self, applied_rule):
    """Returns list of tuples (movement, business_path)"""
    input_movement_list = self._getInputMovementList(applied_rule)
    business_process = applied_rule.getBusinessProcessValue()

    input_movement_and_path_list = []
    business_path_list = []
    for input_movement in input_movement_list:
      for business_path in business_process.getPathValueList(
                          self.getTradePhaseList(),
                          input_movement):
        input_movement_and_path_list.append((input_movement, business_path))
        business_path not in business_path_list and business_path_list \
            .append(business_path)

    if len(business_path_list) > 1:
      raise NotImplementedError

    return input_movement_and_path_list

240
  def _getCompensatedMovementList(self, applied_rule, **kw):
241
    """Compute the difference between prevision and existing movements
242

243
    Immutable movements need compensation, mutable ones needs to be modified
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262

    XXX For now, this implementation is too simple. It could be improved by
    using MovementGroups
    """
    add_list = [] # list of movements to be added
    modify_dict = {} # dict of movements to be modified
    delete_list = [] # list of movements to be deleted

    prevision_list = self._generatePrevisionList(applied_rule, **kw)
    immutable_movement_list, mutable_movement_list, \
        deletable_movement_list = self._getCurrentMovementList(applied_rule,
                                                               **kw)
    movement_list = immutable_movement_list + mutable_movement_list \
                    + deletable_movement_list
    non_matched_list = movement_list[:] # list of remaining movements

    for prevision in prevision_list:
      p_matched_list = []
      for movement in non_matched_list:
263 264 265 266
        if 'order_list' in prevision:
          # applied rule is root, use order link to find movement
          if movement.getOrder() ==  prevision.get('order_list', [''])[0]:
            p_matched_list.append(movement)
267
        else:
268 269 270 271 272 273
          # applied rule is not root one, match
          for prop in self.getMatchingPropertyList():
            if prevision.get(prop) != movement.getProperty(prop):
              break
          else:
            p_matched_list.append(movement)
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293

      # Movements exist, we'll try to make them match the prevision
      if p_matched_list != []:
        # Check the quantity
        m_quantity = 0.0
        for movement in p_matched_list:
          m_quantity += movement.getQuantity()#getCorrectedQuantity()
        if m_quantity != prevision.get('quantity'):
          q_diff = prevision.get('quantity') - m_quantity
          # try to find a movement that can be edited
          for movement in p_matched_list:
            if movement in (mutable_movement_list \
                + deletable_movement_list):
              # mark as requiring modification
              prop_dict = modify_dict.setdefault(movement.getId(), {})
              #prop_dict['quantity'] = movement.getCorrectedQuantity() + \
              prop_dict['quantity'] = movement.getQuantity() + \
                  q_diff
              break
          else:
294 295
            # no modifiable movement was found, need to compensate by quantity
            raise NotImplementedError('Need to generate quantity compensation')
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332

        # Check the date
        for movement in p_matched_list:
          if movement in (mutable_movement_list \
              + deletable_movement_list):
            prop_dict = modify_dict.setdefault(movement.getId(), {})
            for prop in ('start_date', 'stop_date'):
              #XXX should be >= 15
              if prevision.get(prop) != movement.getProperty(prop):
                prop_dict[prop] = prevision.get(prop)
                break

            for k, v in prevision.items():
              if k not in ('quantity', 'start_date', 'stop_date') and \
                      v != movement.getProperty(k):
                prop_dict.setdefault(k, v)

        # update movement lists
        for movement in p_matched_list:
          non_matched_list.remove(movement)

      # No movement matched, we need to create one
      else:
        add_list.append(prevision)

    # delete non matched movements
    for movement in non_matched_list:
      if movement in deletable_movement_list:
        # delete movement
        delete_list.append(movement.getId())
      elif movement in mutable_movement_list:
        # set movement quantity to 0 to make it "void"
        prop_dict = modify_dict.setdefault(movement.getId(), {})
        prop_dict['quantity'] = 0.0
      else:
        # movement not modifiable, we can decide to create a compensation
        # with negative quantity
333 334
        raise NotImplementedError("Tried to delete immutable movement %s" % \
            movement.getRelativeUrl())
335 336 337 338 339
    return (add_list, modify_dict, delete_list)

  def _getExpandablePropertyDict(self, applied_rule, movement, business_path,
      **kw):
    """
340 341
    Return a Dictionary with the Properties used to edit the simulation
    Do NOT overload this method, use _getExpandablePropertyUpdateDict instead
342 343 344 345 346 347 348 349 350 351 352
    """
    property_dict = {}

    default_property_list = self.getExpandablePropertyList()
    for prop in default_property_list:
      property_dict[prop] = movement.getProperty(prop)

    # Arrow
    for base_category in \
        business_path.getSourceBaseCategoryList() +\
        business_path.getDestinationBaseCategoryList():
353 354 355 356 357 358 359
      # XXX: we need to use _list for categories *always*
      category_url = business_path.getDefaultAcquiredCategoryMembership(
          base_category, context=movement)
      if category_url not in ['', None]:
        property_dict['%s_list' % base_category] = [category_url]
      else:
        property_dict['%s_list' % base_category] = []
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
    # Amount
    if business_path.getQuantity():
      property_dict['quantity'] = business_path.getQuantity()
    elif business_path.getEfficiency():
      property_dict['quantity'] = movement.getQuantity() *\
        business_path.getEfficiency()
    else:
      property_dict['quantity'] = movement.getQuantity()

    if movement.getStartDate() == movement.getStopDate():
      property_dict['start_date'] = business_path.getExpectedStartDate(
          movement)
      property_dict['stop_date'] = business_path.getExpectedStopDate(movement)
    else: # XXX shall not be used, but business_path.getExpectedStart/StopDate
          # do not works on second path...
      property_dict['start_date'] = movement.getStartDate()
      property_dict['stop_date'] = movement.getStopDate()

378 379
    # rule specific
    property_dict.update(**self._getExpandablePropertyUpdateDict(applied_rule,
380
      movement, business_path, property_dict))
381 382 383 384

    # save a relation to business path
    property_dict['causality_list'] = [business_path.getRelativeUrl()]

385
    return property_dict
386

387 388
  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
  def expand(self, applied_rule, force=0, **kw):
389 390
    """Generic expand with helpers.
    Do NOT overload, use helpers."""
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    add_list, modify_dict, \
      delete_list = self._getCompensatedMovementList(applied_rule, **kw)

    # delete not needed movements
    for movement_id in delete_list:
      applied_rule._delObject(movement_id)

    # update existing
    for movement, property_dict in modify_dict.items():
      applied_rule[movement].edit(**property_dict)

    # add new ones
    for movement_dict in add_list:
      movement_id = applied_rule._get_id(movement_dict.pop('id', None))
      new_movement = applied_rule.newContent(id=movement_id,
          portal_type=self.movement_type, **movement_dict)

408 409
    for o in applied_rule.objectValues():
      o.expand(**kw)