OrderBuilder.py 36.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2005-2008 Nexedi SA and Contributors. All Rights Reserved.
#                    Romain Courteaud <romain@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility 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
# guarantees 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.
#
##############################################################################

from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet
32
from Products.ERP5Type.Base import Base
33
from Products.ERP5Type.XMLObject import XMLObject
34
from Products.ERP5Type.Core.Predicate import Predicate
35
from Products.ERP5.mixin.builder import BuilderMixin
36 37 38 39 40
from Products.ERP5.Document.Amount import Amount
from Products.ERP5.MovementGroup import MovementGroupNode
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
from DateTime import DateTime
41
from Acquisition import aq_parent, aq_inner, aq_base
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

class CollectError(Exception): pass
class MatrixError(Exception): pass
class DuplicatedPropertyDictKeysError(Exception): pass

class SelectMethodError(Exception): pass
class SelectMovementError(Exception): pass

class OrderBuilder(XMLObject, Amount, Predicate):
  """
    Order Builder objects allow to gather multiple Simulation Movements
    into a single Delivery.

    The initial quantity property of the Delivery Line is calculated by
    summing quantities of related Simulation Movements.

    Order Builder objects are provided with a set a parameters to achieve
    their goal:

    A path definition: source, destination, etc. which defines the general
    kind of movements it applies.

    simulation_select_method which defines how to query all Simulation
    Movements which meet certain criteria (including the above path path
    definition).

    collect_order_list which defines how to group selected movements
    according to gathering rules.

    delivery_select_method which defines how to select existing Delivery
    which may eventually be updated with selected simulation movements.

    delivery_module, delivery_type and delivery_line_type which define the
    module and portal types for newly built Deliveries and Delivery Lines.

    Order Builders can also be provided with optional parameters to
    restrict selection to a given root Applied Rule caused by a single Order
    or to Simulation Movements related to a limited set of existing
    Deliveries.
  """

  # CMF Type Definition
  meta_type = 'ERP5 Order Builder'
  portal_type = 'Order Builder'

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Default Properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.CategoryCore
                    , PropertySheet.DublinCore
                    , PropertySheet.Arrow
                    , PropertySheet.Amount
                    , PropertySheet.Comment
                    , PropertySheet.DeliveryBuilder
                    )

102 103 104 105
  # XXX it would be better to make the base id configurable at
  # each builder.
  matrix_base_id = 'movement'

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
  security.declarePublic('build')
  def build(self, applied_rule_uid=None, movement_relative_url_list=None,
            delivery_relative_url_list=None, movement_list=None, **kw):
    """
      Build deliveries from a list of movements

      Delivery Builders can also be provided with optional parameters to
      restrict selection to a given root Applied Rule caused by a single Order
      or to Simulation Movements related to a limited set of existing
    """
    # Parameter initialization
    if delivery_relative_url_list is None:
      delivery_relative_url_list = []
    # Call a script before building
    self.callBeforeBuildingScript() # XXX-JPS Used ?
    # Select
122 123 124 125 126
    if not movement_list:
      # XXX this code below has a problem of inconsistency in that
      # searchMovementList is unrestricted while passing a list of
      # movements is restricted.
      if not movement_relative_url_list:
127 128 129 130
        movement_list = self.searchMovementList(
                                        delivery_relative_url_list=delivery_relative_url_list,
                                        applied_rule_uid=applied_rule_uid,**kw)
      else:
131 132
        restrictedTraverse = self.getPortalObject().restrictedTraverse
        movement_list = [restrictedTraverse(relative_url) for relative_url \
133
                         in movement_relative_url_list]
134 135
    #Please uncomment if you want to debug.
    #LOG('movement_list', 0, repr(movement_list))
136
    if not movement_list:
137 138 139 140 141 142 143
      return []
    # Collect
    root_group_node = self.collectMovement(movement_list)
    # Build
    delivery_list = self.buildDeliveryList(
                       root_group_node,
                       delivery_relative_url_list=delivery_relative_url_list,
144
                       movement_list=movement_list, **kw)
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
    # Call a script after building
    self.callAfterBuildingScript(delivery_list, movement_list, **kw)
    # XXX Returning the delivery list is probably not necessary
    return delivery_list

  def callBeforeBuildingScript(self):
    """
      Call a script on the module, for example, to remove some
      auto_planned Order.
      This part can only be done with a script, because user may want
      to keep existing auto_planned Order, and only update lines in
      them.
      No activities are used when deleting a object, so, current
      implementation should be OK.
    """
    delivery_module_before_building_script_id = \
        self.getDeliveryModuleBeforeBuildingScriptId()
162
    if delivery_module_before_building_script_id:
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 216 217 218 219 220 221 222 223 224 225 226 227
      delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule())
      getattr(delivery_module, delivery_module_before_building_script_id)()

  def generateMovementListForStockOptimisation(self, **kw):
    from Products.ERP5Type.Document import newTempMovement
    movement_list = []
    for attribute, method in [('node_uid', 'getDestinationUid'),
                              ('section_uid', 'getDestinationSectionUid')]:
      if getattr(self, method)() not in ("", None):
        kw[attribute] = getattr(self, method)()
    # We have to check the inventory for each stock movement date.
    # Inventory can be negative in some date, and positive in futur !!
    # This must be done by subclassing OrderBuilder with a new inventory
    # algorithm.
    sql_list = self.portal_simulation.getFutureInventoryList(
                                                   group_by_variation=1,
                                                   group_by_resource=1,
                                                   group_by_node=1,
                                                   group_by_section=0,
                                                   **kw)
    id_count = 0
    for inventory_item in sql_list:
      # XXX FIXME SQL return None inventory...
      # It may be better to return always good values
      if (inventory_item.inventory is not None):
        dumb_movement = inventory_item.getObject()
        # Create temporary movement
        movement = newTempMovement(self.getPortalObject(),
                                   str(id_count))
        id_count += 1
        movement.edit(
            resource=inventory_item.resource_relative_url,
            variation_category_list=dumb_movement.getVariationCategoryList(),
            destination_value=self.getDestinationValue(),
            destination_section_value=self.getDestinationSectionValue())
        # We can do other test on inventory here
        # XXX It is better if it can be sql parameters
        resource_portal_type = self.getResourcePortalType()
        resource = movement.getResourceValue()
        # FIXME: XXX Those properties are defined on a supply line !!
        # min_flow, max_delay
        min_flow = resource.getMinFlow(0)
        if (resource.getPortalType() == resource_portal_type) and\
           (round(inventory_item.inventory, 5) < min_flow):
          # FIXME XXX getNextNegativeInventoryDate must work
          stop_date = DateTime()+10
#         stop_date = resource.getNextNegativeInventoryDate(
#                               variation_text=movement.getVariationText(),
#                               from_date=DateTime(),
# #                             node_category=node_category,
# #                             section_category=section_category)
#                               node_uid=self.getDestinationUid(),
#                               section_uid=self.getDestinationSectionUid())
          max_delay = resource.getMaxDelay(0)
          movement.edit(
            start_date=DateTime(((stop_date-max_delay).Date())),
            stop_date=DateTime(stop_date.Date()),
            quantity=min_flow-inventory_item.inventory,
            quantity_unit=resource.getQuantityUnit()
            # XXX FIXME define on a supply line
            # quantity_unit
          )
          movement_list.append(movement)
    return movement_list

228 229
  _searchMovementList = BuilderMixin._searchMovementList.im_func

230 231 232 233 234 235 236 237 238 239 240
  @UnrestrictedMethod
  def searchMovementList(self, applied_rule_uid=None, **kw):
    """
      Returns a list of simulation movements (or something similar to
      simulation movements) to construct a new delivery.

      For compatibility, if a simulation select method id is not provided,
      a list of movements for predicting future supplies is returned.
      You should define a simulation select method id, then it will be used
      to calculate the result.
    """
241 242 243
    if self.getSimulationSelectMethodId():
      return self._searchMovementList(**kw)
    return self.generateMovementListForStockOptimisation(**kw) # BBB
244 245 246 247 248 249 250 251 252 253 254 255 256

  def collectMovement(self, movement_list):
    """
      group movements in the way we want. Thanks to this method, we are able
      to retrieve movement classed by order, resource, criterion,....
      movement_list : the list of movement wich we want to group
      class_list : the list of classes used to group movements. The order
                   of the list is important and determines by what we will
                   group movement first
                   Typically, check_list is :
                   [DateMovementGroup,PathMovementGroup,...]
    """
    movement_group_list = self.getMovementGroupList()
257 258 259 260 261 262 263 264 265 266 267 268 269 270

    # Need to find the last branch movement group for separate methods.
    last_line_movement_group = None
    previous_collect_order_group = None
    for movement_group in movement_group_list:
      collect_order_group = movement_group.getCollectOrderGroup()
      if collect_order_group == 'line':
        if previous_collect_order_group == 'delivery' \
                or movement_group.isBranch():
          last_line_movement_group = movement_group
      elif collect_order_group == 'cell':
        break
      previous_collect_order_group = collect_order_group
    if last_line_movement_group is None:
271 272 273 274
      # XXX I think this is an error, but there are many tests which
      # fail, so for now I permit falling back to the last one.
      #raise CollectError('No branch movement group found at %r' % (self,))
      last_line_movement_group = movement_group
275

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
    separate_method_name_list = self.getDeliveryCellSeparateOrderList([])
    root_group_node = MovementGroupNode(
      separate_method_name_list=separate_method_name_list,
      movement_group_list=movement_group_list,
      last_line_movement_group=last_line_movement_group)
    root_group_node.append(movement_list)
    return root_group_node

  def _test(self, instance, movement_group_node_list,
                    divergence_list):
    result = True
    new_property_dict = {}
    for movement_group_node in movement_group_node_list:
      tmp_result, tmp_property_dict = movement_group_node.test(
        instance, divergence_list)
      if not tmp_result:
        result = tmp_result
      new_property_dict.update(tmp_property_dict)
    return result, new_property_dict

296 297
  def _findUpdatableObject(self, instance_list, current_movement_group_node,
          movement_group_node_list, divergence_list):
298 299 300 301 302 303 304 305
    # FIXME this code may generate inconsistent results, because
    # MovementGroupNode.test can return anything else but the
    # property dict. So it would be better to use the test method
    # in all cases. It is, however, not so easy to do, because
    # Movement Group classes might not be prepared for receiving
    # None instead of a real document object. So it would be safer
    # to pass a temp object, but generating a temp object is not 
    # very fast.
306 307
    instance = None
    property_dict = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
308
    if not instance_list:
309
      for movement_group_node in movement_group_node_list:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
310
        for k, v in movement_group_node.getGroupEditDict().iteritems():
311 312 313 314 315 316
          if k in property_dict:
            raise DuplicatedPropertyDictKeysError(k)
          else:
            property_dict[k] = v
    else:
      # we want to check the original delivery first.
317
      movement = current_movement_group_node.getMovementList()[0]
318 319 320
      # XXX in the case of Order Builder, the movement is not always
      # related to simulation, thus it might not have the delivery category.
      # Possibly, this code should be overridden by DeliveryBuilder.
321 322 323 324
      try:
        delivery = movement.getDeliveryValue()
      except AttributeError:
        pass
325
      else:
326 327 328 329 330 331 332 333
        while isinstance(delivery, Base):
          try:
            instance_list.remove(delivery)
          except ValueError:
            pass
          else:
            instance_list.insert(0, delivery)
          delivery = delivery.getParentValue()
334 335 336
      for instance_to_update in instance_list:
        result, property_dict = self._test(
          instance_to_update, movement_group_node_list, divergence_list)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
337
        if result:
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
          instance = instance_to_update
          break
    return instance, property_dict

  @UnrestrictedMethod
  def buildDeliveryList(self, movement_group_node,
                        delivery_relative_url_list=None,
                        movement_list=None, update=True, **kw):
    """
      Build deliveries from a list of movements
    """
    # Parameter initialization
    if delivery_relative_url_list is None:
      delivery_relative_url_list = []
    if movement_list is None:
      movement_list = []
    # Module where we can create new deliveries
    portal = self.getPortalObject()
    delivery_module = getattr(portal, self.getDeliveryModule())
    if update:
358 359
      unrestrictedTraverse = portal.unrestrictedTraverse
      delivery_to_update_list = [unrestrictedTraverse(relative_url) for \
360 361 362
                                 relative_url in delivery_relative_url_list]
      # Deliveries we are trying to update
      delivery_select_method_id = self.getDeliverySelectMethodId()
363 364 365 366
      if delivery_select_method_id:
        delivery_select_method = getattr(self, delivery_select_method_id)
        for brain in delivery_select_method(movement_list=movement_list):
          delivery_to_update_list.append(brain.getObject())
367 368 369 370 371

      # Make sure that the portal type is good.
      delivery_portal_type = self.getDeliveryPortalType()
      delivery_to_update_list = [x for x in delivery_to_update_list \
              if x.getPortalType() == delivery_portal_type]
372 373
    else:
      delivery_to_update_list = []
374

375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
    delivery_list = self._processDeliveryGroup(
                          delivery_module,
                          movement_group_node,
                          self.getDeliveryMovementGroupList(),
                          delivery_to_update_list=delivery_to_update_list,
                          **kw)
    return delivery_list

  def _createDelivery(self, delivery_module, movement_list, activate_kw):
    """
      Create a new delivery in case where a builder may not update
      an existing one.
    """
    new_delivery_id = str(delivery_module.generateNewId())
    delivery = delivery_module.newContent(
      portal_type=self.getDeliveryPortalType(),
      id=new_delivery_id,
      created_by_builder=1,
      activate_kw=activate_kw)
    return delivery

  def _processDeliveryGroup(self, delivery_module, movement_group_node,
                            collect_order_list, movement_group_node_list=None,
                            delivery_to_update_list=None,
                            divergence_list=None,
                            activate_kw=None, force_update=0, **kw):
    """
      Build delivery from a list of movement
    """
    if movement_group_node_list is None:
      movement_group_node_list = []
    if divergence_list is None:
      divergence_list = []
    # Parameter initialization
    if delivery_to_update_list is None:
      delivery_to_update_list = []
    delivery_list = []

413
    if collect_order_list:
414 415
      # Get sorted movement for each delivery
      for grouped_node in movement_group_node.getGroupList():
416 417
        # do not use 'append' or '+=' because they are destructive.
        new_movement_group_node_list = movement_group_node_list + [grouped_node]
418
        new_delivery_list = self._processDeliveryGroup(
419 420 421 422 423 424 425 426
                delivery_module,
                grouped_node,
                collect_order_list[1:],
                movement_group_node_list=new_movement_group_node_list,
                delivery_to_update_list=delivery_to_update_list,
                divergence_list=divergence_list,
                activate_kw=activate_kw,
                force_update=force_update)
427 428 429 430 431 432
        delivery_list.extend(new_delivery_list)
        force_update = 0
    else:
      # Test if we can update a existing delivery, or if we need to create
      # a new one
      delivery, property_dict = self._findUpdatableObject(
433
        delivery_to_update_list, movement_group_node, movement_group_node_list,
434 435 436 437
        divergence_list)

      # if all deliveries are rejected in case of update, we update the
      # first one.
438
      if force_update and delivery is None and delivery_to_update_list:
439 440 441
        delivery = delivery_to_update_list[0]

      if delivery is None:
442 443 444
        if not self.getDeliveryCreatable():
          raise SelectMethodError('No updatable delivery found with %s' \
                  % (self.getPath(),))
445 446 447
        delivery = self._createDelivery(delivery_module,
                                        movement_group_node.getMovementList(),
                                        activate_kw)
448 449 450 451 452
      else:
        # The same delivery should not be updated more than once.
        # Note that it is important to use a destructive method here.
        delivery_to_update_list.remove(delivery)

453 454 455
      # Put properties on delivery
      if property_dict:
        property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
456
        delivery.edit(activate_kw=activate_kw, **property_dict)
457

458 459 460 461 462
      # Then, create delivery lines
      delivery_line_portal_type = self.getDeliveryLinePortalType()
      delivery_line_to_update_list = []
      for line in delivery.contentValues(portal_type=delivery_line_portal_type):
        delivery_line_to_update_list.append(line)
463 464 465 466 467
      for grouped_node in movement_group_node.getGroupList():
        self._processDeliveryLineGroup(
                                delivery,
                                grouped_node,
                                self.getDeliveryLineMovementGroupList()[1:],
468
                                movement_group_node_list=[grouped_node],
469
                                divergence_list=divergence_list,
470
                                delivery_line_to_update_list=delivery_line_to_update_list,
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
                                activate_kw=activate_kw,
                                force_update=force_update)
      delivery_list.append(delivery)
    return delivery_list

  def _createDeliveryLine(self, delivery, movement_list, activate_kw):
    """
      Create a new delivery line in case where a builder may not update
      an existing one.
    """
    new_delivery_line_id = str(delivery.generateNewId())
    delivery_line = delivery.newContent(
      portal_type=self.getDeliveryLinePortalType(),
      id=new_delivery_line_id,
      created_by_builder=1,
      activate_kw=activate_kw)
    return delivery_line

  def _processDeliveryLineGroup(self, delivery, movement_group_node,
                                collect_order_list, movement_group_node_list=None,
                                divergence_list=None,
492
                                delivery_line_to_update_list=None,
493 494 495 496 497 498 499 500
                                activate_kw=None, force_update=0, **kw):
    """
      Build delivery line from a list of movement on a delivery
    """
    if movement_group_node_list is None:
      movement_group_node_list = []
    if divergence_list is None:
      divergence_list = []
501 502
    if delivery_line_to_update_list is None:
      delivery_line_to_update_list = []
503

504
    if collect_order_list and not movement_group_node.getCurrentMovementGroup().isBranch():
505 506
      # Get sorted movement for each delivery line
      for grouped_node in movement_group_node.getGroupList():
507 508
        # do not use 'append' or '+=' because they are destructive.
        new_movement_group_node_list = movement_group_node_list + [grouped_node]
509 510 511 512
        self._processDeliveryLineGroup(
          delivery,
          grouped_node,
          collect_order_list[1:],
513
          movement_group_node_list=new_movement_group_node_list,
514
          divergence_list=divergence_list,
515
          delivery_line_to_update_list=delivery_line_to_update_list,
516 517 518 519 520 521
          activate_kw=activate_kw,
          force_update=force_update)
    else:
      # Test if we can update an existing line, or if we need to create a new
      # one
      delivery_line, property_dict = self._findUpdatableObject(
522 523
        delivery_line_to_update_list, movement_group_node,
        movement_group_node_list, divergence_list)
524 525
      if delivery_line is not None:
        update_existing_line = 1
526
        delivery_line_to_update_list.remove(delivery_line)
527 528 529 530 531 532 533 534 535 536
      else:
        # Create delivery line
        update_existing_line = 0
        delivery_line = self._createDeliveryLine(
                delivery,
                movement_group_node.getMovementList(),
                activate_kw)
      # Put properties on delivery line
      if property_dict:
        property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
537 538
        delivery_line.edit(force_update=1, activate_kw=activate_kw, 
                **property_dict)
539 540

      if movement_group_node.getCurrentMovementGroup().isBranch():
541 542 543 544
        delivery_line_portal_type = self.getDeliveryLinePortalType()
        nested_delivery_line_to_update_list = []
        for line in delivery_line.contentValues(portal_type=delivery_line_portal_type):
          nested_delivery_line_to_update_list.append(line)
545 546 547 548 549
        for grouped_node in movement_group_node.getGroupList():
          self._processDeliveryLineGroup(
            delivery_line,
            grouped_node,
            collect_order_list[1:],
550
            movement_group_node_list=[grouped_node],
551
            divergence_list=divergence_list,
552
            delivery_line_to_update_list=nested_delivery_line_to_update_list,
553 554 555 556 557
            activate_kw=activate_kw,
            force_update=force_update)
        return

      # Update variation category list on line
558
      variation_category_set = set(delivery_line.getVariationCategoryList())
559
      for movement in movement_group_node.getMovementList():
560 561
        variation_category_set.update(movement.getVariationCategoryList())
      variation_category_list = sorted(variation_category_set)
562 563
      delivery_line.edit(variation_category_list=variation_category_list,
              activate_kw=activate_kw)
564 565 566 567 568
      # Then, create delivery movement (delivery cell or complete delivery
      # line)
      grouped_node_list = movement_group_node.getGroupList()
      # If no group is defined for cell, we need to continue, in order to
      # save the quantity value
569
      if grouped_node_list:
570 571 572
        base_id = self.matrix_base_id
        getCell = delivery_line.getCell
        delivery_movement_to_update_list = []
573 574 575 576 577 578 579 580
        cell_key_list = delivery_line.getCellKeyList(base_id=base_id)
        if cell_key_list:
          for cell_key in cell_key_list:
            cell = getCell(base_id=base_id, *cell_key)
            if cell is not None:
              delivery_movement_to_update_list.append(cell)
        else:
          delivery_movement_to_update_list.append(delivery_line)
581 582 583 584 585
        for grouped_node in grouped_node_list:
          self._processDeliveryCellGroup(
                                    delivery_line,
                                    grouped_node,
                                    self.getDeliveryCellMovementGroupList()[1:],
586
                                    movement_group_node_list=[grouped_node],
587 588
                                    update_existing_line=update_existing_line,
                                    divergence_list=divergence_list,
589
                                    delivery_movement_to_update_list=delivery_movement_to_update_list,
590 591 592 593 594 595 596
                                    activate_kw=activate_kw,
                                    force_update=force_update)
      else:
        self._processDeliveryCellGroup(
                                  delivery_line,
                                  movement_group_node,
                                  [],
597
                                  movement_group_node_list=[],
598 599
                                  update_existing_line=update_existing_line,
                                  divergence_list=divergence_list,
600
                                  delivery_movement_to_update_list=[delivery_line],
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
                                  activate_kw=activate_kw,
                                  force_update=force_update)

  def _createDeliveryCell(self, delivery_line, movement, activate_kw,
                          base_id, cell_key):
    """
      Create a new delivery cell in case where a builder may not update
      an existing one.
    """
    cell = delivery_line.newCell(base_id=base_id,
                                 portal_type=self.getDeliveryCellPortalType(),
                                 activate_kw=activate_kw,
                                 *cell_key)
    return cell

  def _processDeliveryCellGroup(self, delivery_line, movement_group_node,
                                collect_order_list, movement_group_node_list=None,
                                update_existing_line=0,
                                divergence_list=None,
620
                                delivery_movement_to_update_list=None,
621 622 623 624 625 626 627
                                activate_kw=None, force_update=0):
    """
      Build delivery cell from a list of movement on a delivery line
      or complete delivery line
    """
    if movement_group_node_list is None:
      movement_group_node_list = []
628 629
    if delivery_movement_to_update_list is None:
      delivery_movement_to_update_list = []
630 631 632
    if divergence_list is None:
      divergence_list = []

633
    if collect_order_list:
634 635
      # Get sorted movement for each delivery line
      for grouped_node in movement_group_node.getGroupList():
636
        new_movement_group_node_list = movement_group_node_list + [grouped_node]
637 638 639 640
        self._processDeliveryCellGroup(
          delivery_line,
          grouped_node,
          collect_order_list[1:],
641
          movement_group_node_list=new_movement_group_node_list,
642 643
          update_existing_line=update_existing_line,
          divergence_list=divergence_list,
644
          delivery_movement_to_update_list=delivery_movement_to_update_list,
645 646 647 648 649 650 651 652
          activate_kw=activate_kw,
          force_update=force_update)
    else:
      movement_list = movement_group_node.getMovementList()
      if len(movement_list) != 1:
        raise CollectError, "DeliveryBuilder: %s unable to distinct those\
              movements: %s" % (self.getId(), str(movement_list))
      else:
653
        base_id = self.matrix_base_id
654 655 656 657 658 659 660 661 662
        object_to_update = None
        # We need to initialize the cell
        update_existing_movement = 0
        movement = movement_list[0]
        # decide if we create a cell or if we update the line
        # Decision can only be made with line matrix range:
        # because matrix range can be empty even if line variation category
        # list is not empty
        property_dict = {}
663
        if not delivery_line.getCellKeyList(base_id=base_id):
664
          # update line
665
          dummy, property_dict = self._findUpdatableObject(
666 667
            delivery_movement_to_update_list, movement_group_node,
            movement_group_node_list, divergence_list)
668 669 670 671
          if delivery_movement_to_update_list:
            if update_existing_line:
              update_existing_movement = 1
            del delivery_movement_to_update_list[:]
672
          else:
673 674 675
            # XXX probably an exception should be raised here.
            pass
          object_to_update = delivery_line
676 677
        else:
          object_to_update, property_dict = self._findUpdatableObject(
678 679
            delivery_movement_to_update_list, movement_group_node,
            movement_group_node_list, divergence_list)
680 681 682 683 684
          if object_to_update is not None:
            # We update a existing cell
            # delivery_ratio of new related movement to this cell
            # must be updated to 0.
            update_existing_movement = 1
685
            delivery_movement_to_update_list.remove(object_to_update)
686
          else:
687 688 689 690 691 692 693
            # create a new cell
            cell_key = movement.getVariationCategoryList(
                    omit_optional_variation=1)
            if not delivery_line.hasCell(base_id=base_id, *cell_key):
              cell = self._createDeliveryCell(delivery_line, movement,
                                              activate_kw, base_id, cell_key)
              vcl = movement.getVariationCategoryList()
694 695
              # _createDeliveryCell calls reindexObject, so no need to use
              # edit here.
696 697 698 699 700 701 702 703 704 705 706
              cell._edit(category_list=vcl,
                         # XXX hardcoded value
                         mapped_value_property_list=('quantity', 'price'),
                         membership_criterion_category_list=vcl,
                         membership_criterion_base_category_list=movement.\
                                               getVariationBaseCategoryList())
            else:
              raise MatrixError, 'Cell: %s already exists on %s' % \
                    (str(cell_key), str(delivery_line))
            object_to_update = cell

707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
        self._setDeliveryMovementProperties(
                            object_to_update, movement, property_dict,
                            update_existing_movement=update_existing_movement,
                            force_update=force_update, activate_kw=activate_kw)

  def _setDeliveryMovementProperties(self, delivery_movement,
                                     simulation_movement, property_dict,
                                     update_existing_movement=0,
                                     force_update=0, activate_kw=None):
    """
      Initialize or update delivery movement properties.
    """
    if not update_existing_movement or force_update:
      # Now, only 1 movement is possible, so copy from this movement
      # XXX hardcoded value
      if getattr(simulation_movement, 'getMappedProperty', None) is not None:
        property_dict['quantity'] = simulation_movement.getMappedProperty('quantity')
      else:
        property_dict['quantity'] = simulation_movement.getQuantity()
      property_dict['price'] = simulation_movement.getPrice()
      # Update properties on object (quantity, price...)
728 729
      delivery_movement.edit(force_update=1, activate_kw=activate_kw,
              **property_dict)
730 731 732 733 734 735

  @UnrestrictedMethod
  def callAfterBuildingScript(self, delivery_list, movement_list=None, **kw):
    """
      Call script on each delivery built.
    """
736
    if not delivery_list:
737 738 739 740 741 742 743 744
      return
    # Parameter initialization
    if movement_list is None:
      movement_list = []
    delivery_after_generation_script_id = \
                              self.getDeliveryAfterGenerationScriptId()
    related_simulation_movement_path_list = \
                              [x.getPath() for x in movement_list]
745
    if delivery_after_generation_script_id:
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
      for delivery in delivery_list:
        script = getattr(delivery, delivery_after_generation_script_id)
        # BBB: Only Python Scripts were used in the past, and they might not
        # accept an arbitrary argument. So to keep compatibility,
        # check if it can take the new parameter safely, only when
        # the callable object is a Python Script.
        safe_to_pass_parameter = True
        meta_type = getattr(script, 'meta_type', None)
        if meta_type == 'Script (Python)':
          # check if the script accepts related_simulation_movement_path_list
          safe_to_pass_parameter = False
          for param in script.params().split(','):
            param = param.split('=', 1)[0].strip()
            if param == 'related_simulation_movement_path_list' \
                    or param.startswith('**'):
              safe_to_pass_parameter = True
              break

        if safe_to_pass_parameter:
          script(related_simulation_movement_path_list=related_simulation_movement_path_list)
        else:
          script()

  security.declareProtected(Permissions.AccessContentsInformation,
                           'getMovementGroupList')
  def getMovementGroupList(self, portal_type=None, collect_order_group=None,
                            **kw):
    """
    Return a list of movement groups sorted by collect order group and index.
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
776
    portal = self.getPortalObject()
777
    if portal_type is None:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
      portal_type = portal.getPortalMovementGroupTypeList()

    if collect_order_group is None:
      category_index_dict = {}
      for i in portal.portal_categories.collect_order_group.contentValues():
        category_index_dict[i.getId()] = i.getIntIndex()

      def getMovementGroupKey(movement_group):
        return (category_index_dict.get(movement_group.getCollectOrderGroup()),
                movement_group.getIntIndex())

      filter_dict = dict(portal_type=portal_type)
      movement_group_list = self.contentValues(filter=filter_dict)
    else:
      def getMovementGroupKey(movement_group):
        return movement_group.getIntIndex()

      filter_dict = dict(portal_type=portal_type)
      movement_group_list = []
      for movement_group in self.contentValues(filter=filter_dict):
        if movement_group.getCollectOrderGroup() == collect_order_group:
          movement_group_list.append(movement_group)

    return sorted(movement_group_list, key=getMovementGroupKey)
802 803 804 805 806 807 808 809 810 811 812 813 814

  # XXX category name is hardcoded.
  def getDeliveryMovementGroupList(self, **kw):
    return self.getMovementGroupList(collect_order_group='delivery')

  # XXX category name is hardcoded.
  def getDeliveryLineMovementGroupList(self, **kw):
    return self.getMovementGroupList(collect_order_group='line')

  # XXX category name is hardcoded.
  def getDeliveryCellMovementGroupList(self, **kw):
    return self.getMovementGroupList(collect_order_group='cell')

815 816
  # XXX this method is not used in OrderBuilder but in DeliveryBuilder.
  # So it should perhaps be moved to DeliveryBuilder.
817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
  def _searchUpByPortalType(self, obj, portal_type):
    limit_portal_type = self.getPortalObject().getPortalType()
    while obj is not None:
      obj_portal_type = obj.getPortalType()
      if obj_portal_type == portal_type:
        break
      elif obj_portal_type == limit_portal_type:
        obj = None
        break
      else:
        obj = aq_parent(aq_inner(obj))
    return obj

  # for backward compatibilities.
  _deliveryGroupProcessing = _processDeliveryGroup
  _deliveryLineGroupProcessing = _processDeliveryLineGroup
  _deliveryCellGroupProcessing = _processDeliveryCellGroup