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
import zope.interface
32
from ExtensionClass import Base
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33 34 35 36
from AccessControl import ClassSecurityInfo
from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire
from Products.CMFCore.utils import getToolByName

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

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

49 50 51 52 53 54 55 56 57 58 59 60 61
  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]

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

72 73 74 75 76 77 78 79 80 81 82 83
    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)
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

  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)

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 135
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


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

139 140
  security.declareProtected(Permissions.View, 'getTransformation')
  def getTransformation(self, movement=None, applied_rule=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
141
    """
142
    Return transformation related to used by the applied rule.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
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 215
    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)

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

226 227 228 229 230 231 232 233
  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\
234
                                      .getAggregatedAmountList()])
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 260
    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

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

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  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)
280
    factory = self.getFactory()
281
    factory.product = dict(
282 283 284 285 286 287 288
      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.
289
    amount_dict = {}
290
    for amount in transformation.getAggregatedAmountList():
291 292 293
      phase = amount.getTradePhase()

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

297 298 299 300 301 302
      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():
303
      phase_path_list = business_process.getPathValueList(phase)
Romain Courteaud's avatar
Romain Courteaud committed
304
      """
305
      XXX: In this context, we assume quantity as ratio,
306
      but this "quantity as ratio" is consistent with transformation.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
307
      """
308 309
      if sum(map(lambda path: path.getQuantity(), phase_path_list)) != 1:
        raise TransformationRuleError,\
310 311
              "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
312

313
      for path in phase_path_list:
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
        # 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)()

329 330 331
        start_date = path.getExpectedStartDate(explanation)
        stop_date = path.getExpectedStopDate(explanation)
        predecessor_remaining_phase_list = path.getPredecessorValue()\
332 333
          .getRemainingTradePhaseList(explanation,
                                      trade_phase_list=trade_phase_list)
334
        successor_remaining_phase_list = path.getSuccessorValue()\
335 336 337 338 339 340 341 342 343 344 345
          .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.
346 347 348
            quantity=factory.product['quantity'] * path.getQuantity(),
            source_section=source_section,
            destination_section=destination_section,
349 350
            destination=destination,
            trade_phase_value_list=successor_remaining_phase_list)
351
        else:
352 353 354 355 356 357 358 359 360 361 362
          # 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
363 364 365 366 367
          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
368 369 370 371 372 373
          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\
374 375
             last_prop_dict['source_section'] != source_section or\
             last_prop_dict['destination_section'] != destination_section or\
376 377 378 379
             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
380

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

        # consumed movement
394
        for amount in amount_list:
395 396 397 398 399
          factory.requestConsumed(
            causality_value=path,
            start_date=start_date,
            stop_date=stop_date,
            resource=amount.getResource(),
400
            quantity=factory.product['quantity'] * amount.getQuantity()\
401 402
              / amount.getEfficiency() * path.getQuantity(),
            quantity_unit=amount.getQuantityUnit(),
403 404 405
            source_section=source_section,
            destination_section=destination_section,
            source=source,
406
            trade_phase=path.getTradePhase())
Romain Courteaud's avatar
Romain Courteaud committed
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 436
    """
    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.
437
      quantity=factory.product['quantity'] * path.getQuantity(),
438 439 440
      **last_prop_dict)

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