TransformationRule.py 17.1 KB
Newer Older
1
# -*- coding:utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
##############################################################################
#
4
# Copyright (c) 2002-2009 Nexedi SARL 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 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#
# 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.
#
##############################################################################

31
from ExtensionClass import Base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32 33 34 35
from AccessControl import ClassSecurityInfo
from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
from Products.CMFCore.utils import getToolByName

36
from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from Products.ERP5.Document.Rule import Rule
38
from Products.ERP5.Document.SimulationMovement import SimulationMovement
39
from Products.ERP5Type.Errors import TransformationRuleError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40

41 42 43 44 45 46
class MovementFactory:
  def getRequestList(self):
    """
    return the list of a request which to be used to apply movements
    """
    raise NotImplementedError, 'Must be implemented'
47

48 49 50 51 52 53 54 55 56 57 58 59 60
  def _getCausalityList(self, causality=None, causality_value=None,
                        causality_list=None, causality_value_list=None,
                        **kw):
    if causality is not None:
      return [causality]
    elif causality_value is not None:
      return [causality_value.getRelativeUrl()]
    elif causality_list is not None:
      return causality_list
    elif causality_value_list is not None:
      return [causality_value.getRelativeUrl()
              for causality_value in causality_value_list]

61 62 63 64
  def makeMovements(self, applied_rule):
    """
    make movements under the applied_rule by requests
    """
65 66 67 68 69 70
    movement_dict = {}
    for movement in applied_rule.objectValues(
        portal_type="Simulation Movement"):
      key = tuple(sorted(movement.getCausalityList()))
      movement_dict[key] = movement

71 72 73 74 75 76 77 78 79 80 81 82
    for request in self.getRequestList():
      # get movement by causality
      key = tuple(sorted(self._getCausalityList(**request)))
      movement = movement_dict.get(key, None)
      # when no exist
      if movement is None:
        movement = applied_rule.newContent(portal_type="Simulation Movement")
      # update
      if movement.isFrozen():
        self.makeDifferentMovement(movement, **request)
      else:
        movement.edit(**request)
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

  def _requestNetQuantity(self, request):
    quantity = request.get('quantity', None)
    efficiency = request.get('efficiency', None)
    if efficiency in (0, 0.0, None, ''):
      efficiency = 1.0
    return float(quantity) / efficiency

  def makeDifferentMovement(self, movement, **request):
    """
    make different movement, which is based on original movement.
    this implementation just focus about quantity.
    """
    applied_rule = movement.getParentValue()
    request['quantity'] = self._requestNetQuantity(request)\
                          - movement.getNetQuantity()
    if request['quantity'] != 0:
      diff_movement = applied_rule.newContent(portal_type="Simulation Movement")
      diff_movement.edit(**request)

103

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
class TransformationMovementFactory(MovementFactory):
  def __init__(self):
    self.product = None # base information to use for making movements
    self.produced_list = list()
    self.consumed_list = list()

  def requestProduced(self, **produced):
    self.produced_list.append(produced)

  def requestConsumed(self, **consumed):
    self.consumed_list.append(consumed)

  def getRequestList(self):
    """
    return the list of a request which to be used to apply movements
    """
    _list = []
    """
    produced quantity should be represented by minus quantity on movement.
    because plus quantity is consumed.
    """ 
    for (request_list, sign) in ((self.produced_list, -1),
                                 (self.consumed_list, 1)):
      for request in request_list:
        d = self.product.copy()
        d.update(request)
        d['quantity'] *= sign
        _list.append(d)
    return _list


135 136
class TransformationRuleMixin(Base):
  security = ClassSecurityInfo()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
137

138 139
  security.declareProtected(Permissions.View, 'getTransformation')
  def getTransformation(self, movement=None, applied_rule=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
140
    """
141
    Return transformation related to used by the applied rule.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
142
    """
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    if movement is None and applied_rule is not None:
      movement = applied_rule.getParentValue()

    order_movement = movement.getRootSimulationMovement().getOrderValue()
    explanation = self.getExplanation(movement=movement,
                                      applied_rule=applied_rule)
    # find line recursively
    order_line = order_movement
    while order_line.getParentValue() != explanation:
      order_line = order_line.getParentValue()

    script = order_line._getTypeBasedMethod('_getTransformation')
    if script is not None:
      transformation = script()
    else:
      line_transformation = order_line.objectValues(
        portal_type=self.getPortalTransformationTypeList())
      if len(line_transformation) == 1:
        transformation = line_transformation[0]
      else:
        transformation = order_line.getSpecialiseValue(
          portal_type=self.getPortalTransformationTypeList())

    if transformation.getResource() == movement.getResource():
      return transformation

  security.declareProtected(Permissions.View, 'getBusinessProcess')
  def getBusinessProcess(self, **kwargs):
    """
    Return business process related to root causality.
    """
    explanation = self.getExplanation(**kwargs)
    if explanation is not None:
      specialise = explanation.getSpecialiseValue()
      business_process_type_list = self.getPortalBusinessProcessTypeList()
      # because trade condition can be specialised
      while specialise is not None and \
            specialise.getPortalType() not in business_process_type_list:
        specialise = specialise.getSpecialiseValue()
      return specialise

  security.declareProtected(Permissions.View, 'getRootExplanation')
  def getRootExplanation(self, business_process):
    """
    the method of ProductionOrderRule returns most tail path of business process
    """
    if business_process is not None:
      for business_path in business_process.contentValues(
        portal_type=self.getPortalBusinessPathTypeList()):
        if business_path.isDeliverable():
          return business_path

  security.declareProtected(Permissions.View, 'getExplanation')
  def getExplanation(self, movement=None, applied_rule=None):
    if applied_rule is not None:
      return applied_rule.getRootAppliedRule().getCausalityValue()
    else:
      return movement.getRootSimulationMovement()\
             .getOrderValue().getExplanationValue()


class TransformationRule(TransformationRuleMixin, Rule):
  """
  """

  # CMF Type Definition
  meta_type = 'ERP5 Transformation Rule'
  portal_type = 'Transformation Rule'
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

215 216
  __implements__ = ( interfaces.IPredicate,
                     interfaces.IRule )
217 218
  # Default Properties
  property_sheets = ( PropertySheet.Base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
219 220 221
                      , PropertySheet.XMLObject
                      , PropertySheet.CategoryCore
                      , PropertySheet.DublinCore
222
                      , PropertySheet.Task
Jean-Paul Smets's avatar
Jean-Paul Smets committed
223 224
                      )

225 226 227 228 229 230 231 232
  def getHeadProductionPathList(self, transformation, business_process):
    """
    Return list of path which is head of transformation trade_phases

    this method assumes trade_phase of head paths is only one
    """
    production_trade_phase_set = set([amount.getTradePhase()
                                      for amount in transformation\
233
                                      .getAggregatedAmountList()])
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    head_path_list = []
    for state in business_process.objectValues(
      portal_type=self.getPortalBusinessStateTypeList()):
      if len(state.getSuccessorRelatedValueList()) == 0:
        head_path_list.extend(state.getPredecessorRelatedValueList())

    result_list = []
    for path in head_path_list:
      result_list += self._getHeadPathByTradePhaseList(path, production_trade_phase_set)

    return map(lambda t: t[0], filter(lambda t: t != (None, None), result_list))

  def _getHeadPathByTradePhaseList(self, path, trade_phase_set):
    _set = set(path.getTradePhaseList())
    if _set & trade_phase_set:
      return [(path, _set & trade_phase_set)]

    successor_node = path.getSuccessorValue()
    if successor_node is None:
      return [(None, None)]

    _list = []
    for next_path in successor_node.getPredecessorRelatedValueList():
      _list += self._getHeadPathByTradePhaseList(next_path, trade_phase_set)
    return _list

260 261 262
  def getFactory(self):
    return TransformationMovementFactory()

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
  security.declareProtected(Permissions.ModifyPortalContent, 'expand')
  def expand(self, applied_rule, **kw):
    """
    """
    parent_movement = applied_rule.getParentValue()

    transformation = self.getTransformation(movement=parent_movement)
    business_process = self.getBusinessProcess(movement=parent_movement)
    explanation = self.getExplanation(movement=parent_movement)

    # get all trade_phase of the Business Process
    trade_phase_list = business_process.getTradePhaseList()

    # get head of production path from business process with trade_phase_list
    head_production_path_list = self.getHeadProductionPathList(transformation,
                                                               business_process)
279
    factory = self.getFactory()
280
    factory.product = dict(
281 282 283 284 285 286 287
      resource=transformation.getResource(),
      quantity=parent_movement.getNetQuantity(),
      quantity_unit=parent_movement.getQuantityUnit(),
      variation_category_list=parent_movement.getVariationCategoryList(),
      variation_property_dict=parent_movement.getVariationPropertyDict(),)

    # consumed amounts are sorted by phase, but not ordered.
288
    amount_dict = {}
289
    for amount in transformation.getAggregatedAmountList():
290 291 292
      phase = amount.getTradePhase()

      if phase not in trade_phase_list:
293
        raise TransformationRuleError,\
294 295
              "Trade phase %r is not part of Business Process %r" % (phase, business_process)

296 297 298 299 300 301
      amount_dict.setdefault(phase, [])
      amount_dict[phase].append(amount)

    last_phase_path_list = list() # to keep phase_path_list
    last_prop_dict = dict()
    for (phase, amount_list) in amount_dict.items():
302
      phase_path_list = business_process.getPathValueList(phase)
Romain Courteaud's avatar
Romain Courteaud committed
303
      """
304
      XXX: In this context, we assume quantity as ratio,
305
      but this "quantity as ratio" is consistent with transformation.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
306
      """
307 308
      if sum(map(lambda path: path.getQuantity(), phase_path_list)) != 1:
        raise TransformationRuleError,\
309 310
              "sum ratio of Trade Phase %r of Business Process %r is not one"\
              % (phase, business_process)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
311

312
      for path in phase_path_list:
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
        # source, source_section
        source_section = path.getSourceSection() # only support a static access 
        source_method_id = path.getSourceMethodId()
        if source_method_id is None:
          source = path.getSource()
        else:
          source = getattr(path, source_method_id)()
        # destination, destination_section
        destination_section = path.getDestinationSection() # only support a static access 
        destination_method_id = path.getDestinationMethodId()
        if destination_method_id is None:
          destination = path.getDestination()
        else:
          destination = getattr(path, destination_method_id)()

328 329 330
        start_date = path.getExpectedStartDate(explanation)
        stop_date = path.getExpectedStopDate(explanation)
        predecessor_remaining_phase_list = path.getPredecessorValue()\
331 332
          .getRemainingTradePhaseList(explanation,
                                      trade_phase_list=trade_phase_list)
333
        successor_remaining_phase_list = path.getSuccessorValue()\
334 335 336 337 338 339 340 341 342 343 344
          .getRemainingTradePhaseList(explanation,
                                      trade_phase_list=trade_phase_list)

        # checking which is not last path of transformation
        if len(successor_remaining_phase_list) != 0:
          # partial produced movement
          factory.requestProduced(
            causality_value=path,
            start_date=start_date,
            stop_date=stop_date,
            # when last path of transformation, path.getQuantity() will be return 1.
345 346 347
            quantity=factory.product['quantity'] * path.getQuantity(),
            source_section=source_section,
            destination_section=destination_section,
348 349
            destination=destination,
            trade_phase_value_list=successor_remaining_phase_list)
350
        else:
351 352 353 354 355 356 357 358 359 360 361
          # for making movement of last product of the transformation
          last_phase_path_list.append(path)

          # path params must be same
          if last_prop_dict.get('start_date', None) is None:
            last_prop_dict['start_date'] = start_date
          if last_prop_dict.get('stop_date', None) is None:
            last_prop_dict['stop_date'] = stop_date
          # trade phase of product is must be empty []
          if last_prop_dict.get('trade_phase_value_list', None) is None:
            last_prop_dict['trade_phase_value_list'] = successor_remaining_phase_list
362 363 364 365 366
          if last_prop_dict.get('source_section', None) is None:
            last_prop_dict['source_section'] = source_section
          # for the source, it is not need, because the produced.
          if last_prop_dict.get('destination_section', None) is None:
            last_prop_dict['destination_section'] = destination_section
367 368 369 370 371 372
          if last_prop_dict.get('destination', None) is None:
            last_prop_dict['destination'] = destination

          if last_prop_dict['start_date'] != start_date or\
             last_prop_dict['stop_date'] != stop_date or\
             last_prop_dict['trade_phase_value_list'] != successor_remaining_phase_list or\
373 374
             last_prop_dict['source_section'] != source_section or\
             last_prop_dict['destination_section'] != destination_section or\
375 376 377 378
             last_prop_dict['destination'] != destination:
            raise TransformationRuleError,\
              """Returned property is different on Transformation %r and Business Process %r"""\
              % (transformation, business_process)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
379

380 381
        # when the path is part of production but not first, consume previous partial product
        if path not in head_production_path_list:
382 383 384 385
          factory.requestConsumed(
            causality_value=path,
            start_date=start_date,
            stop_date=stop_date,
386 387 388 389
            quantity=factory.product['quantity'] * path.getQuantity(),
            source_section=source_section,
            destination_section=destination_section,
            source=source,
390 391 392
            trade_phase_value_list=predecessor_remaining_phase_list)

        # consumed movement
393
        for amount in amount_list:
394 395 396 397 398
          factory.requestConsumed(
            causality_value=path,
            start_date=start_date,
            stop_date=stop_date,
            resource=amount.getResource(),
399
            quantity=factory.product['quantity'] * amount.getQuantity()\
400 401
              / amount.getEfficiency() * path.getQuantity(),
            quantity_unit=amount.getQuantityUnit(),
402 403 404
            source_section=source_section,
            destination_section=destination_section,
            source=source,
405
            trade_phase=path.getTradePhase())
Romain Courteaud's avatar
Romain Courteaud committed
406

407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
    """
    valid graph for transformation
    a --- b --- c

    a --
        \
         X b
        /
    c --

    invalid graph
    a ------- b
    c ------- d

        -- b
       /
    a X
       \
        -- c
    """
    # when empty
    if last_phase_path_list is None or len(last_phase_path_list) == 0:
      raise TransformationRuleError,\
            """Could not make the product by Transformation %r and Business Process %r,
which last_phase_path_list is empty.""" % (transformation, business_process)

    factory.requestProduced(
      causality_value_list=last_phase_path_list,
      # when last path of transformation, path.getQuantity() will be return 1.
436
      quantity=factory.product['quantity'] * path.getQuantity(),
437 438 439
      **last_prop_dict)

    factory.makeMovements(applied_rule)
440
    Rule.expand(self, applied_rule, **kw)