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
class FakeMovementError(Exception) : pass
Romain Courteaud's avatar
Romain Courteaud committed
35
class MovementGroupError(Exception) : pass
36

37
class MovementGroupNode:
38 39 40 41
  # 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.
42 43 44
  def __init__(self, movement_group_list=None, movement_list=None,
               last_line_movement_group=None,
               separate_method_name_list=[], movement_group=None):
45 46
    self._movement_list = []
    self._group_list = []
47 48 49
    self._movement_group = movement_group
    self._movement_group_list = movement_group_list
    self._last_line_movement_group = last_line_movement_group
50
    self._separate_method_name_list = separate_method_name_list
51 52 53 54 55 56 57 58 59 60 61
    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)
62
    self._group_list.append(nested_instance)
63 64 65 66 67 68 69 70 71 72 73 74
    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)
75
          else:
76 77 78 79 80 81
            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
82
        self._movement_list, split_movement = self._separate(movement)
83 84 85 86 87
        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
88

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

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

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

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

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

117 118 119 120
  def getMovementList(self):
    """
      Return movement list in the current group
    """
121 122 123 124 125 126 127 128 129
    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

130 131 132 133 134 135 136 137 138 139
  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

140
  def test(self, movement, divergence_list):
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    # 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]
163
      return self._movement_group.test(movement, self._property_dict,
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
                                               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

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

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

197 198 199 200 201
      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,
202 203 204 205 206
                                  added_movement=added_movement)
        if rejected_movement is None:
          added_movement = None
        else:
          break
207

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

  ########################################################
  # Separate methods
  ########################################################
  def _genericCalculation(self, movement, added_movement=None):
214
    """ Generic creation of FakeMovement
215 216 217 218 219 220 221 222 223
    """
    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):
224
    """ Create a new movement with a average price
225
    """
226
    new_movement = self._genericCalculation(movement,
227 228 229 230
                                            added_movement=added_movement)
    new_movement.setPriceMethod("getAveragePrice")
    return new_movement, None

231
  def calculateSeparatePrice(self, movement, added_movement=None):
232
    """ Separate movements which have different price
233
    """
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
    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
250 251
    return movement, added_movement

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

260 261
  def __repr__(self):
    repr_str = '<%s object at 0x%x\n' % (self.__class__.__name__, id(self))
262
    repr_str += ' _movement_group = %r,\n' % self._movement_group
263 264
    if getattr(self, '_property_dict', None) is not None:
      repr_str += ' _property_dict = %r,\n' % self._property_dict
265 266 267 268 269 270
    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:
271 272
      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
273 274
    return repr_str

275
allow_class(MovementGroupNode)
276

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

284 285
  def __init__(self, movement_list):
    """
Alexandre Boeglin's avatar
Alexandre Boeglin committed
286
      Create a fake movement and store the list of real movements
287 288 289 290 291 292 293 294
    """
    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
295
      raise ValueError, "FakeMovement used where it should not."
296 297 298 299 300
    # 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()
301
    reference_variation_category_list.sort()
302 303
    for movement in movement_list[1:]:
      variation_category_list = movement.getVariationCategoryList()
304 305 306
      variation_category_list.sort()
      if variation_category_list != reference_variation_category_list:
        raise ValueError, "FakeMovement not well used."
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323

  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
324

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

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  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):
    """
348
      Only use to return a short description of one movement
349 350 351 352 353
      (if user did not configure DeliveryBuilder well...).
      Be careful.
    """
    return self.__movement_list[0].getRelativeUrl()

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

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

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

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

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

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

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

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

426 427 428 429 430 431 432 433 434 435 436 437 438 439
  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

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

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

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

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

470 471
  def getVariationBaseCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
472 473 474 475
    """
      Return variation base category list
      Which must be shared by all movement
    """
476 477 478 479 480 481
    #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

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

485 486
  def getVariationCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
487 488 489 490
    """
      Return variation base category list
      Which must be shared by all movement
    """
491 492 493 494 495 496
    #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

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

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

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

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

523 524 525 526 527
  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
528

529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
# 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
546 547 548

  def __init__(self,movement,**kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
    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)

569 570 571 572 573
# 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
574 575 576
  def getIntIndex(self,movement):
    order_value = movement.getOrderValue()
    int_index = 0
577
    if order_value is not None:
Sebastien Robin's avatar
Sebastien Robin committed
578 579 580 581 582 583
      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

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

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

allow_class(IntIndexMovementGroup)
596

Romain Courteaud's avatar
Romain Courteaud committed
597
class TransformationAppliedRuleCausalityMovementGroup(RootMovementGroup):
598
  """
Romain Courteaud's avatar
Romain Courteaud committed
599
  Groups movement that comes from simulation movement that shares the
600
  same Production Applied Rule.
Romain Courteaud's avatar
Romain Courteaud committed
601 602 603 604 605 606 607 608 609 610 611
  """
  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 """
612
    transformation_applied_rule = movement.getParentValue()
Romain Courteaud's avatar
Romain Courteaud committed
613 614 615
    transformation_rule = transformation_applied_rule.getSpecialiseValue()
    if transformation_rule.getPortalType() != 'Transformation Rule':
      raise MovementGroupError, 'movement! %s' % movement.getPath()
616
    # XXX Dirty hardcoded
Romain Courteaud's avatar
Romain Courteaud committed
617 618 619
    production_movement = transformation_applied_rule.pr
    production_packing_list = production_movement.getExplanationValue()
    return production_packing_list.getRelativeUrl()
620

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

allow_class(TransformationAppliedRuleCausalityMovementGroup)

626 627
class ParentExplanationMovementGroup(RootMovementGroup): pass

Romain Courteaud's avatar
Romain Courteaud committed
628 629 630 631 632 633 634 635 636 637 638
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)