MovementGroup.py 22.3 KB
Newer Older
Sebastien Robin's avatar
Sebastien Robin committed
1 2
##############################################################################
#
3
# Copyright (c) 2002-2008 Nexedi SA and Contributors. All Rights Reserved.
Sebastien Robin's avatar
Sebastien Robin committed
4
#                    Sebastien Robin <seb@nexedi.com>
Sebastien Robin's avatar
Sebastien Robin committed
5
#                    Yoshinori Okuji <yo@nexedi.com>
6
#                    Romain Courteaud <romain@nexedi.com>
Sebastien Robin's avatar
Sebastien Robin 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
Sebastien Robin's avatar
Sebastien Robin 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
Sebastien Robin's avatar
Sebastien Robin 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
from warnings import warn
32
from Products.PythonScripts.Utility import allow_class
Sebastien Robin's avatar
Sebastien Robin committed
33

34 35
class MovementRejected(Exception) : pass
class FakeMovementError(Exception) : pass
Romain Courteaud's avatar
Romain Courteaud committed
36
class MovementGroupError(Exception) : pass
37

38
class MovementGroupNode:
39 40 41 42
  # XXX last_line_movement_group is a wrong name. Actually, it is
  # the last delivery movement group. This parameter is used to
  # pick up a branching point of making separate lines when
  # a separate method requests separating movements.
43 44 45
  def __init__(self, movement_group_list=None, movement_list=None,
               last_line_movement_group=None,
               separate_method_name_list=[], movement_group=None):
46 47
    self._movement_list = []
    self._group_list = []
48 49 50
    self._movement_group = movement_group
    self._movement_group_list = movement_group_list
    self._last_line_movement_group = last_line_movement_group
51
    self._separate_method_name_list = separate_method_name_list
52 53 54 55 56 57 58 59 60 61 62
    if movement_list is not None :
      self.append(movement_list)

  def _appendGroup(self, movement_list, property_dict):
    nested_instance = MovementGroupNode(
      movement_group=self._movement_group_list[0],
      movement_group_list=self._movement_group_list[1:],
      last_line_movement_group=self._last_line_movement_group,
      separate_method_name_list=self._separate_method_name_list)
    nested_instance.setGroupEdit(**property_dict)
    split_movement_list = nested_instance.append(movement_list)
63
    self._group_list.append(nested_instance)
64 65 66 67 68 69 70 71 72 73 74 75
    return split_movement_list

  def append(self, movement_list):
    all_split_movement_list = []
    if len(self._movement_group_list):
      for separate_movement_list, property_dict in \
          self._movement_group_list[0].separate(movement_list):
        split_movement_list = self._appendGroup(separate_movement_list,
                                                property_dict)
        if len(split_movement_list):
          if self._movement_group == self._last_line_movement_group:
            self.append(split_movement_list)
76
          else:
77 78 79 80 81 82
            all_split_movement_list.extend(split_movement_list)
    else:
      self._movement_list.append(movement_list[0])
      for movement in movement_list[1:]:
        # We have a conflict here, because it is forbidden to have
        # 2 movements on the same node group
83
        self._movement_list, split_movement = self._separate(movement)
84 85 86 87 88
        if split_movement is not None:
          # We rejected a movement, we need to put it on another line
          # Or to create a new one
          all_split_movement_list.append(split_movement)
    return all_split_movement_list
Sebastien Robin's avatar
Sebastien Robin committed
89

90
  def getGroupList(self):
91
    return self._group_list
Sebastien Robin's avatar
Sebastien Robin committed
92

93 94
  def setGroupEdit(self, **kw):
    """
95
      Store properties for the futur created object
96 97
    """
    self._property_dict = kw
Sebastien Robin's avatar
Sebastien Robin committed
98

Romain Courteaud's avatar
Romain Courteaud committed
99 100
  def updateGroupEdit(self, **kw):
    """
101
      Update properties for the futur created object
Romain Courteaud's avatar
Romain Courteaud committed
102 103 104
    """
    self._property_dict.update(kw)

105 106
  def getGroupEditDict(self):
    """
107
      Get property dict for the futur created object
108
    """
109 110 111 112 113
    property_dict = getattr(self, '_property_dict', {}).copy()
    for key in property_dict.keys():
      if key.startswith('_'):
        del(property_dict[key])
    return property_dict
114

115 116 117
  def getCurrentMovementGroup(self):
    return self._movement_group

118 119 120 121
  def getMovementList(self):
    """
      Return movement list in the current group
    """
122 123 124 125 126 127 128 129 130
    movement_list = []
    group_list = self.getGroupList()
    if len(group_list) == 0:
      return self._movement_list
    else:
      for group in group_list:
        movement_list.extend(group.getMovementList())
      return movement_list

131 132 133 134 135 136 137 138 139 140
  def getMovement(self):
    """
      Return first movement of the movement list in the current group
    """
    movement = self.getMovementList()[0]
    if movement.__class__.__name__ == 'FakeMovement':
      return movement.getMovementList()[0]
    else:
      return movement

141
  def test(self, movement, divergence_list):
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    # Try to check if movement is updatable or not.
    #
    # 1. if Divergence has no scope: update anyway.
    # 2. if Divergence has a scope: update in related Movement Group only.
    #
    # return value is:
    #   [updatable? (True/False), property dict for update]
    if self._movement_group is not None:
      property_list = []
      if len(divergence_list):
        divergence_scope = self._movement_group.getDivergenceScope()
        if divergence_scope is None:
          # Update anyway (eg. CausalityAssignmentMovementGroup etc.)
          pass
        else:
          related_divergence_list = [
            x for x in divergence_list \
            if divergence_scope == x.divergence_scope and \
            self.hasSimulationMovement(x.simulation_movement)]
          if not len(related_divergence_list):
            return True, {}
          property_list = [x.tested_property for x in related_divergence_list]
164
      return self._movement_group.test(movement, self._property_dict,
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
                                               property_list=property_list)
    else:
      return True, {}

  def getDivergenceScope(self):
    if self._movement_group is not None:
      return self._movement_group.getDivergenceScope()
    else:
      return None

  def hasSimulationMovement(self, simulation_movement):
    for movement in self.getMovementList():
      if movement.__class__.__name__ == "FakeMovement":
        if simulation_movement in movement.getMovementList():
          return True
      elif simulation_movement == movement:
        return True
    return False

184 185 186 187 188 189
  def _separate(self, movement):
    """
      Separate 2 movements on a node group
    """
    movement_list = self.getMovementList()
    if len(movement_list) != 1:
190
      raise ValueError, "Can separate only 2 movements"
191 192 193 194 195 196
    else:
      old_movement = self.getMovementList()[0]

      new_stored_movement = old_movement
      added_movement = movement
      rejected_movement = None
Sebastien Robin's avatar
Sebastien Robin committed
197

198 199 200 201 202
      for separate_method_name in self._separate_method_name_list:
        method = getattr(self, separate_method_name)

        new_stored_movement,\
        rejected_movement= method(new_stored_movement,
203 204 205 206 207
                                  added_movement=added_movement)
        if rejected_movement is None:
          added_movement = None
        else:
          break
208

209
      return [new_stored_movement], rejected_movement
210 211 212 213 214

  ########################################################
  # Separate methods
  ########################################################
  def _genericCalculation(self, movement, added_movement=None):
215
    """ Generic creation of FakeMovement
216 217 218 219 220 221 222 223 224
    """
    if added_movement is not None:
      # Create a fake movement
      new_movement = FakeMovement([movement, added_movement])
    else:
      new_movement = movement
    return new_movement

  def calculateAveragePrice(self, movement, added_movement=None):
225
    """ Create a new movement with a average price
226
    """
227
    new_movement = self._genericCalculation(movement,
228 229 230 231
                                            added_movement=added_movement)
    new_movement.setPriceMethod("getAveragePrice")
    return new_movement, None

232
  def calculateSeparatePrice(self, movement, added_movement=None):
233
    """ Separate movements which have different price
234
    """
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
    if added_movement is not None:
      # XXX To prevent float rounding issue, we round the price with an
      # arbirary precision before comparision.
      movement_price = movement.getPrice()
      if movement_price is not None:
        movement_price = round(movement_price, 5)
      added_movement_price = added_movement.getPrice()
      if added_movement_price is not None:
        added_movement_price = round(added_movement_price, 5)

      if movement_price == added_movement_price:
        new_movement = self._genericCalculation(movement,
                                                added_movement=added_movement)
        new_movement.setPriceMethod('getAveragePrice')
        new_movement.setQuantityMethod("getAddQuantity")
        return new_movement, None
251 252
    return movement, added_movement

253
  def calculateAddQuantity(self, movement, added_movement=None):
254
    """ Create a new movement with the sum of quantity
255
    """
256
    new_movement = self._genericCalculation(movement,
257 258 259
                                            added_movement=added_movement)
    new_movement.setQuantityMethod("getAddQuantity")
    return new_movement, None
Sebastien Robin's avatar
Sebastien Robin committed
260

261 262
  def __repr__(self):
    repr_str = '<%s object at 0x%x\n' % (self.__class__.__name__, id(self))
263
    repr_str += ' _movement_group = %r,\n' % self._movement_group
264 265
    if getattr(self, '_property_dict', None) is not None:
      repr_str += ' _property_dict = %r,\n' % self._property_dict
266 267 268 269 270 271
    if self._movement_list:
      repr_str += ' _movement_list = %r,\n' % self._movement_list
    if self._group_list:
      repr_str += ' _group_list = [\n%s]>' % (
        '\n'.join(['   %s' % x for x in (',\n'.join([repr(i) for i in self._group_list])).split('\n')]))
    else:
272 273
      repr_str += ' _last_line_movement_group = %r,\n' % self._last_line_movement_group
      repr_str += ' _separate_method_name_list = %r>' % self._separate_method_name_list
274 275
    return repr_str

276
allow_class(MovementGroupNode)
277

278 279
class FakeMovement:
  """
Alexandre Boeglin's avatar
Alexandre Boeglin committed
280
    A fake movement which simulates some methods on a movement needed
281
    by DeliveryBuilder.
Alexandre Boeglin's avatar
Alexandre Boeglin committed
282
    It contains a list of real ERP5 Movements and can modify them.
283
  """
284

285 286
  def __init__(self, movement_list):
    """
Alexandre Boeglin's avatar
Alexandre Boeglin committed
287
      Create a fake movement and store the list of real movements
288 289 290 291 292 293 294 295
    """
    self.__price_method = None
    self.__quantity_method = None
    self.__movement_list = []
    for movement in movement_list:
      self.append(movement)
    # This object must not be use when there is not 2 or more movements
    if len(movement_list) < 2:
Alexandre Boeglin's avatar
Alexandre Boeglin committed
296
      raise ValueError, "FakeMovement used where it should not."
297 298 299 300 301
    # All movements must share the same getVariationCategoryList
    # So, verify and raise a error if not
    # But, if DeliveryBuilder is well configured, this can never append ;)
    reference_variation_category_list = movement_list[0].\
                                           getVariationCategoryList()
302
    reference_variation_category_list.sort()
303 304
    for movement in movement_list[1:]:
      variation_category_list = movement.getVariationCategoryList()
305 306 307
      variation_category_list.sort()
      if variation_category_list != reference_variation_category_list:
        raise ValueError, "FakeMovement not well used."
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324

  def append(self, movement):
    """
      Append movement to the movement list
    """
    if movement.__class__.__name__ == "FakeMovement":
      self.__movement_list.extend(movement.getMovementList())
      self.__price_method = movement.__price_method
      self.__quantity_method = movement.__quantity_method
    else:
      self.__movement_list.append(movement)

  def getMovementList(self):
    """
      Return content movement list
    """
    return self.__movement_list
325

326
  def setDeliveryValue(self, object):
327 328 329 330
    """
      Set Delivery value for each movement
    """
    for movement in self.__movement_list:
331 332
      movement.edit(delivery_value=object)

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
  def getDeliveryValue(self):
    """
      Only use to test if all movement are not linked (if user did not
      configure DeliveryBuilder well...).
      Be careful.
    """
    result = None
    for movement in self.__movement_list:
      mvt_delivery = movement.getDeliveryValue()
      if mvt_delivery is not None:
        result = mvt_delivery
        break
    return result

  def getRelativeUrl(self):
    """
349
      Only use to return a short description of one movement
350 351 352 353 354
      (if user did not configure DeliveryBuilder well...).
      Be careful.
    """
    return self.__movement_list[0].getRelativeUrl()

355 356 357 358 359 360
  def setDeliveryRatio(self, delivery_ratio):
    """
      Calculate delivery_ratio
    """
    total_quantity = 0
    for movement in self.__movement_list:
361
      total_quantity += movement.getMappedProperty('quantity')
362

363 364
    if total_quantity != 0:
      for movement in self.__movement_list:
365
        quantity = movement.getMappedProperty('quantity')
366
        movement.edit(delivery_ratio=quantity*delivery_ratio/total_quantity)
367 368
    else:
      # Distribute equally ratio to all movement
369
      mvt_ratio = 1. / len(self.__movement_list)
370
      for movement in self.__movement_list:
371
        movement.edit(delivery_ratio=mvt_ratio)
372

373 374 375 376
  def getPrice(self):
    """
      Return calculated price
    """
377 378 379 380
    if self.__price_method is not None:
      return getattr(self, self.__price_method)()
    else:
      return None
381

382 383 384 385 386 387 388 389 390 391 392
  def setPriceMethod(self, method):
    """
      Set the price method
    """
    self.__price_method = method

  def getQuantity(self):
    """
      Return calculated quantity
    """
    return getattr(self, self.__quantity_method)()
393

394 395 396 397 398 399 400 401
  def setQuantityMethod(self, method):
    """
      Set the quantity method
    """
    self.__quantity_method = method

  def getAveragePrice(self):
    """
402
      Return average price
403
    """
404 405 406 407 408
    price_dict = self._getPriceDict()
    if len(price_dict) == 1:
      return price_dict.keys()[0]
    return sum(price * quantity for price, quantity in price_dict.items()) / \
           float(sum(price_dict.values()))
409 410 411 412 413 414 415

  def getAddQuantity(self):
    """
      Return the total quantity
    """
    total_quantity = 0
    for movement in self.getMovementList():
416 417
      getMappedProperty = getattr(movement, 'getMappedProperty', None)
      if getMappedProperty is None:
418
        quantity = movement.getQuantity()
419 420 421
      else:
        quantity = getMappedProperty('quantity')
      if quantity:
422
        total_quantity += quantity
423 424
    return total_quantity

425 426 427 428 429 430 431 432 433 434 435 436 437 438
  def _getPriceDict(self):
    price_dict = {}
    for movement in self.getMovementList():
      getMappedProperty = getattr(movement, 'getMappedProperty', None)
      if getMappedProperty is None:
        quantity = movement.getQuantity()
      else:
        quantity = getMappedProperty('quantity')
      if quantity:
        price = movement.getPrice() or 0
        quantity += price_dict.setdefault(price, 0)
        price_dict[price] = quantity
    return price_dict

439 440
  def getAddPrice(self):
    """
441
      Return total price
442
    """
443 444
    price_dict = self._getPriceDict()
    return sum(price * quantity for price, quantity in price_dict.items())
445 446 447 448 449 450 451 452

  def recursiveReindexObject(self):
    """
      Reindex all movements
    """
    for movement in self.getMovementList():
      movement.recursiveReindexObject()

453 454 455 456 457 458 459
  def immediateReindexObject(self):
    """
      Reindex immediately all movements
    """
    for movement in self.getMovementList():
      movement.immediateReindexObject()

460 461 462 463 464 465 466 467 468
  def getPath(self):
    """
      Return the movements path list
    """
    path_list = []
    for movement in self.getMovementList():
      path_list.append(movement.getPath())
    return path_list

469 470
  def getVariationBaseCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
471 472 473 474
    """
      Return variation base category list
      Which must be shared by all movement
    """
475 476 477 478 479 480
    #XXX backwards compatibility
    if omit_option_base_category is not None:
      warn("Please use omit_optional_variation instead of"\
          " omit_option_base_category.", DeprecationWarning)
      omit_optional_variation = omit_option_base_category

481
    return self.__movement_list[0].getVariationBaseCategoryList(
482
        omit_optional_variation=omit_optional_variation, **kw)
483

484 485
  def getVariationCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
486 487 488 489
    """
      Return variation base category list
      Which must be shared by all movement
    """
490 491 492 493 494 495
    #XXX backwards compatibility
    if omit_option_base_category is not None:
      warn("Please use omit_optional_variation instead of"\
          " omit_option_base_category.", DeprecationWarning)
      omit_optional_variation = omit_option_base_category

496
    return self.__movement_list[0].getVariationCategoryList(
497
        omit_optional_variation=omit_optional_variation, **kw)
498

499 500 501 502 503 504
  def getMappedProperty(self, property):
    if property == 'quantity':
      return self.getQuantity()
    else:
      raise NotImplementedError

505
  def edit(self, activate_kw=None, **kw):
506
    """
507 508
      Written in order to call edit in delivery builder,
      as it is the generic way to modify object.
509 510 511

      activate_kw is here for compatibility reason with Base.edit,
      it will not be used here.
512
    """
513 514 515 516 517 518
    for key in kw.keys():
      if key == 'delivery_ratio':
        self.setDeliveryRatio(kw[key])
      elif key == 'delivery_value':
        self.setDeliveryValue(kw[key])
      else:
519
        raise FakeMovementError,\
520
              "Could not call edit on Fakemovement with parameters: %r" % key
521

522 523 524 525 526
  def __repr__(self):
    repr_str = '<%s object at 0x%x for %r' % (self.__class__.__name__,
                                              id(self),
                                              self.getMovementList())
    return repr_str
Jean-Paul Smets's avatar
Jean-Paul Smets committed
527

528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
# The following classes are not ported to Document/XxxxMovementGroup.py yet.

class RootMovementGroup(MovementGroupNode):
  pass

class SplitResourceMovementGroup(RootMovementGroup):

  def __init__(self, movement, **kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
    self.resource = movement.getResource()

  def test(self, movement):
    return movement.getResource() == self.resource

allow_class(SplitResourceMovementGroup)

class OptionMovementGroup(RootMovementGroup):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
545 546 547

  def __init__(self,movement,**kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
    option_base_category_list = movement.getPortalOptionBaseCategoryList()
    self.option_category_list = movement.getVariationCategoryList(
                                  base_category_list=option_base_category_list)
    if self.option_category_list is None:
      self.option_category_list = []
    self.option_category_list.sort()
    # XXX This is very bad, but no choice today.
    self.setGroupEdit(industrial_phase_list = self.option_category_list)

  def test(self,movement):
    option_base_category_list = movement.getPortalOptionBaseCategoryList()
    movement_option_category_list = movement.getVariationCategoryList(
                              base_category_list=option_base_category_list)
    if movement_option_category_list is None:
      movement_option_category_list = []
    movement_option_category_list.sort()
    return movement_option_category_list == self.option_category_list

allow_class(OptionMovementGroup)

568 569 570 571 572
# XXX This should not be here
# I (seb) have commited this because movement groups are not
# yet configurable through the zope web interface
class IntIndexMovementGroup(RootMovementGroup):

Sebastien Robin's avatar
Sebastien Robin committed
573 574 575
  def getIntIndex(self,movement):
    order_value = movement.getOrderValue()
    int_index = 0
576
    if order_value is not None:
Sebastien Robin's avatar
Sebastien Robin committed
577 578 579 580 581 582
      if "Line" in order_value.getPortalType():
        int_index = order_value.getIntIndex()
      elif "Cell" in order_value.getPortalType():
        int_index = order_value.getParentValue().getIntIndex()
    return int_index

583 584
  def __init__(self,movement,**kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
Sebastien Robin's avatar
Sebastien Robin committed
585 586
    int_index = self.getIntIndex(movement)
    self.int_index = int_index
587
    self.setGroupEdit(
Sebastien Robin's avatar
Sebastien Robin committed
588
        int_index=int_index
589 590 591
    )

  def test(self,movement):
592
    return self.getIntIndex(movement) == self.int_index
593 594

allow_class(IntIndexMovementGroup)
595

Romain Courteaud's avatar
Romain Courteaud committed
596
class TransformationAppliedRuleCausalityMovementGroup(RootMovementGroup):
597
  """
Romain Courteaud's avatar
Romain Courteaud committed
598
  Groups movement that comes from simulation movement that shares the
599
  same Production Applied Rule.
Romain Courteaud's avatar
Romain Courteaud committed
600 601 602 603 604 605 606 607 608 609 610
  """
  def __init__(self, movement, **kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
    explanation_relative_url = self._getExplanationRelativeUrl(movement)
    self.explanation = explanation_relative_url
    explanation_value = movement.getPortalObject().restrictedTraverse(
                                                    explanation_relative_url)
    self.setGroupEdit(causality_value=explanation_value)

  def _getExplanationRelativeUrl(self, movement):
    """ Get the order value for a movement """
611
    transformation_applied_rule = movement.getParentValue()
Romain Courteaud's avatar
Romain Courteaud committed
612 613 614
    transformation_rule = transformation_applied_rule.getSpecialiseValue()
    if transformation_rule.getPortalType() != 'Transformation Rule':
      raise MovementGroupError, 'movement! %s' % movement.getPath()
615
    # XXX Dirty hardcoded
Romain Courteaud's avatar
Romain Courteaud committed
616 617 618
    production_movement = transformation_applied_rule.pr
    production_packing_list = production_movement.getExplanationValue()
    return production_packing_list.getRelativeUrl()
619

Romain Courteaud's avatar
Romain Courteaud committed
620 621 622 623 624
  def test(self,movement):
    return self._getExplanationRelativeUrl(movement) == self.explanation

allow_class(TransformationAppliedRuleCausalityMovementGroup)

625 626
class ParentExplanationMovementGroup(RootMovementGroup): pass

Romain Courteaud's avatar
Romain Courteaud committed
627 628 629 630 631 632 633 634 635 636 637
class ParentExplanationCausalityMovementGroup(ParentExplanationMovementGroup):
  """
  Like ParentExplanationMovementGroup, and set the causality.
  """
  def __init__(self, movement, **kw):
    ParentExplanationMovementGroup.__init__(self, movement=movement, **kw)
    self.updateGroupEdit(
        causality_value = self.explanation_value
    )

allow_class(ParentExplanationCausalityMovementGroup)