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

86
  def getGroupList(self):
87
    return self._group_list
Sebastien Robin's avatar
Sebastien Robin committed
88

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

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

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

111 112 113
  def getCurrentMovementGroup(self):
    return self._movement_group

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

127 128 129 130 131 132 133 134 135 136
  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

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

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

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

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

205
      return [new_stored_movement], rejected_movement
206 207 208 209 210

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

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

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

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

272
allow_class(MovementGroupNode)
273

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

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

  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
321

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

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

351 352 353 354 355 356 357 358
  def setDeliveryRatio(self, delivery_ratio):
    """
      Calculate delivery_ratio
    """
    total_quantity = 0
    for movement in self.__movement_list:
      total_quantity += movement.getQuantity()

359 360 361
    if total_quantity != 0:
      for movement in self.__movement_list:
        quantity = movement.getQuantity()
362
        movement.edit(delivery_ratio=quantity*delivery_ratio/total_quantity)
363 364
    else:
      # Distribute equally ratio to all movement
365
      mvt_ratio = 1. / len(self.__movement_list)
366
      for movement in self.__movement_list:
367
        movement.edit(delivery_ratio=mvt_ratio)
368

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

378 379 380 381 382 383 384 385 386 387 388
  def setPriceMethod(self, method):
    """
      Set the price method
    """
    self.__price_method = method

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

390 391 392 393 394 395 396 397
  def setQuantityMethod(self, method):
    """
      Set the quantity method
    """
    self.__quantity_method = method

  def getAveragePrice(self):
    """
398
      Return average price
399
    """
400 401 402
    if self.getAddQuantity()>0:
      return (self.getAddPrice() / self.getAddQuantity())
    return 0.0
403 404 405 406 407 408 409

  def getAddQuantity(self):
    """
      Return the total quantity
    """
    total_quantity = 0
    for movement in self.getMovementList():
410 411 412
      quantity = movement.getQuantity()
      if quantity != None:
        total_quantity += quantity
413 414 415 416
    return total_quantity

  def getAddPrice(self):
    """
417
      Return total price
418 419 420
    """
    total_price = 0
    for movement in self.getMovementList():
421 422 423 424
      quantity = movement.getQuantity()
      price = movement.getPrice()
      if (quantity is not None) and (price is not None):
        total_price += (quantity * price)
425 426 427 428 429 430 431 432 433
    return total_price

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

434 435 436 437 438 439 440
  def immediateReindexObject(self):
    """
      Reindex immediately all movements
    """
    for movement in self.getMovementList():
      movement.immediateReindexObject()

441 442 443 444 445 446 447 448 449
  def getPath(self):
    """
      Return the movements path list
    """
    path_list = []
    for movement in self.getMovementList():
      path_list.append(movement.getPath())
    return path_list

450 451
  def getVariationBaseCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
452 453 454 455
    """
      Return variation base category list
      Which must be shared by all movement
    """
456 457 458 459 460 461
    #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

462
    return self.__movement_list[0].getVariationBaseCategoryList(
463
        omit_optional_variation=omit_optional_variation, **kw)
464

465 466
  def getVariationCategoryList(self, omit_optional_variation=0,
      omit_option_base_category=None, **kw):
467 468 469 470
    """
      Return variation base category list
      Which must be shared by all movement
    """
471 472 473 474 475 476
    #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

477
    return self.__movement_list[0].getVariationCategoryList(
478
        omit_optional_variation=omit_optional_variation, **kw)
479

480
  def edit(self, activate_kw=None, **kw):
481
    """
482 483
      Written in order to call edit in delivery builder,
      as it is the generic way to modify object.
484 485 486

      activate_kw is here for compatibility reason with Base.edit,
      it will not be used here.
487
    """
488 489 490 491 492 493
    for key in kw.keys():
      if key == 'delivery_ratio':
        self.setDeliveryRatio(kw[key])
      elif key == 'delivery_value':
        self.setDeliveryValue(kw[key])
      else:
494
        raise FakeMovementError,\
495
              "Could not call edit on Fakemovement with parameters: %r" % key
496

497 498 499 500 501
  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
502

503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
# 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
520 521 522

  def __init__(self,movement,**kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
    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)

543 544 545 546 547
# 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
548 549 550
  def getIntIndex(self,movement):
    order_value = movement.getOrderValue()
    int_index = 0
551
    if order_value is not None:
Sebastien Robin's avatar
Sebastien Robin committed
552 553 554 555 556 557
      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

558 559
  def __init__(self,movement,**kw):
    RootMovementGroup.__init__(self, movement=movement, **kw)
Sebastien Robin's avatar
Sebastien Robin committed
560 561
    int_index = self.getIntIndex(movement)
    self.int_index = int_index
562
    self.setGroupEdit(
Sebastien Robin's avatar
Sebastien Robin committed
563
        int_index=int_index
564 565 566
    )

  def test(self,movement):
567
    return self.getIntIndex(movement) == self.int_index
568 569

allow_class(IntIndexMovementGroup)
570

Romain Courteaud's avatar
Romain Courteaud committed
571
class TransformationAppliedRuleCausalityMovementGroup(RootMovementGroup):
572
  """
Romain Courteaud's avatar
Romain Courteaud committed
573
  Groups movement that comes from simulation movement that shares the
574
  same Production Applied Rule.
Romain Courteaud's avatar
Romain Courteaud committed
575 576 577 578 579 580 581 582 583 584 585
  """
  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 """
586
    transformation_applied_rule = movement.getParentValue()
Romain Courteaud's avatar
Romain Courteaud committed
587 588 589
    transformation_rule = transformation_applied_rule.getSpecialiseValue()
    if transformation_rule.getPortalType() != 'Transformation Rule':
      raise MovementGroupError, 'movement! %s' % movement.getPath()
590
    # XXX Dirty hardcoded
Romain Courteaud's avatar
Romain Courteaud committed
591 592 593
    production_movement = transformation_applied_rule.pr
    production_packing_list = production_movement.getExplanationValue()
    return production_packing_list.getRelativeUrl()
594

Romain Courteaud's avatar
Romain Courteaud committed
595 596 597 598 599
  def test(self,movement):
    return self._getExplanationRelativeUrl(movement) == self.explanation

allow_class(TransformationAppliedRuleCausalityMovementGroup)

600 601
class ParentExplanationMovementGroup(RootMovementGroup): pass

Romain Courteaud's avatar
Romain Courteaud committed
602 603 604 605 606 607 608 609 610 611 612
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)