OrderBuilder.py 33.7 KB
Newer Older
Romain Courteaud's avatar
Romain Courteaud committed
1 2
##############################################################################
#
3
# Copyright (c) 2005-2008 Nexedi SA and Contributors. All Rights Reserved.
Romain Courteaud's avatar
Romain Courteaud committed
4 5 6
#                    Romain Courteaud <romain@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
7
# programmers who take the whole responsibility of assessing all potential
Romain Courteaud's avatar
Romain Courteaud committed
8 9
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
10
# guarantees and support are strongly adviced to contract a Free Software
Romain Courteaud's avatar
Romain Courteaud committed
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# 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
30
from Products.ERP5Type import Permissions, PropertySheet
Romain Courteaud's avatar
Romain Courteaud committed
31 32 33
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5.Document.Predicate import Predicate
from Products.ERP5.Document.Amount import Amount
34 35
from Products.ERP5.MovementGroup import MovementGroupNode
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
36
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
37
from DateTime import DateTime
38
from Acquisition import aq_parent, aq_inner
Romain Courteaud's avatar
Romain Courteaud committed
39

40 41
class CollectError(Exception): pass
class MatrixError(Exception): pass
42
class DuplicatedPropertyDictKeysError(Exception): pass
43

44 45 46
class SelectMethodError(Exception): pass
class SelectMovementError(Exception): pass

Romain Courteaud's avatar
Romain Courteaud committed
47 48 49
class OrderBuilder(XMLObject, Amount, Predicate):
  """
    Order Builder objects allow to gather multiple Simulation Movements
50
    into a single Delivery.
Romain Courteaud's avatar
Romain Courteaud committed
51 52 53 54

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

55
    Order Builder objects are provided with a set a parameters to achieve
Romain Courteaud's avatar
Romain Courteaud committed
56 57
    their goal:

58
    A path definition: source, destination, etc. which defines the general
Romain Courteaud's avatar
Romain Courteaud committed
59 60
    kind of movements it applies.

61 62
    simulation_select_method which defines how to query all Simulation
    Movements which meet certain criteria (including the above path path
Romain Courteaud's avatar
Romain Courteaud committed
63 64
    definition).

65
    collect_order_list which defines how to group selected movements
Romain Courteaud's avatar
Romain Courteaud committed
66 67
    according to gathering rules.

68
    delivery_select_method which defines how to select existing Delivery
Romain Courteaud's avatar
Romain Courteaud committed
69 70
    which may eventually be updated with selected simulation movements.

71
    delivery_module, delivery_type and delivery_line_type which define the
Romain Courteaud's avatar
Romain Courteaud committed
72 73
    module and portal types for newly built Deliveries and Delivery Lines.

74
    Order Builders can also be provided with optional parameters to
Romain Courteaud's avatar
Romain Courteaud committed
75
    restrict selection to a given root Applied Rule caused by a single Order
76
    or to Simulation Movements related to a limited set of existing
Romain Courteaud's avatar
Romain Courteaud committed
77 78 79 80 81 82 83 84 85
    Deliveries.
  """

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

  # Declarative security
  security = ClassSecurityInfo()
86
  security.declareObjectProtected(Permissions.AccessContentsInformation)
Romain Courteaud's avatar
Romain Courteaud committed
87 88 89 90 91 92 93 94 95 96 97

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

99
  security.declarePublic('build')
100
  def build(self, applied_rule_uid=None, movement_relative_url_list=None,
101
            delivery_relative_url_list=None, movement_list=None, **kw):
Romain Courteaud's avatar
Romain Courteaud committed
102 103 104 105 106 107 108
    """
      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
    """
109 110 111 112 113
    # Parameter initialization
    if delivery_relative_url_list is None:
      delivery_relative_url_list = []
    # Call a script before building
    self.callBeforeBuildingScript()
Romain Courteaud's avatar
Romain Courteaud committed
114
    # Select
115 116 117 118 119
    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:
120 121 122 123
        movement_list = self.searchMovementList(
                                        delivery_relative_url_list=delivery_relative_url_list,
                                        applied_rule_uid=applied_rule_uid,**kw)
      else:
124 125
        restrictedTraverse = self.getPortalObject().restrictedTraverse
        movement_list = [restrictedTraverse(relative_url) for relative_url \
126
                         in movement_relative_url_list]
127
    if not movement_list:
128
      return []
Romain Courteaud's avatar
Romain Courteaud committed
129
    # Collect
130
    root_group_node = self.collectMovement(movement_list)
Romain Courteaud's avatar
Romain Courteaud committed
131 132
    # Build
    delivery_list = self.buildDeliveryList(
133
                       root_group_node,
Romain Courteaud's avatar
Romain Courteaud committed
134
                       delivery_relative_url_list=delivery_relative_url_list,
135
                       movement_list=movement_list, **kw)
136
    # Call a script after building
137
    self.callAfterBuildingScript(delivery_list, movement_list, **kw)
138
    # XXX Returning the delivery list is probably not necessary
Romain Courteaud's avatar
Romain Courteaud committed
139 140
    return delivery_list

141
  def callBeforeBuildingScript(self):
Romain Courteaud's avatar
Romain Courteaud committed
142
    """
143
      Call a script on the module, for example, to remove some
144
      auto_planned Order.
145 146
      This part can only be done with a script, because user may want
      to keep existing auto_planned Order, and only update lines in
147 148 149 150 151 152
      them.
      No activities are used when deleting a object, so, current
      implementation should be OK.
    """
    delivery_module_before_building_script_id = \
        self.getDeliveryModuleBeforeBuildingScriptId()
153
    if delivery_module_before_building_script_id:
154
      delivery_module = getattr(self.getPortalObject(), self.getDeliveryModule())
155
      getattr(delivery_module, delivery_module_before_building_script_id)()
Romain Courteaud's avatar
Romain Courteaud committed
156

157
  def generateMovementListForStockOptimisation(self, **kw):
158
    from Products.ERP5Type.Document import newTempMovement
Romain Courteaud's avatar
Romain Courteaud committed
159
    movement_list = []
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
    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
181
        movement = newTempMovement(self.getPortalObject(),
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
                                   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(
209
            start_date=DateTime(((stop_date-max_delay).Date())),
210
            stop_date=DateTime(stop_date.Date()),
211 212 213 214 215 216
            quantity=min_flow-inventory_item.inventory,
            quantity_unit=resource.getQuantityUnit()
            # XXX FIXME define on a supply line
            # quantity_unit
          )
          movement_list.append(movement)
Romain Courteaud's avatar
Romain Courteaud committed
217
    return movement_list
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247

  @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.
    """
    method_id = self.getSimulationSelectMethodId()
    if not method_id:
      # XXX compatibility
      return self.generateMovementListForStockOptimisation(**kw)

    select_method = getattr(self.getPortalObject(), method_id)
    movement_list = select_method(**kw)

    # Make sure that movements are not duplicated.
    movement_set = set()
    for movement in movement_list:
      if movement in movement_set:
        raise SelectMethodError('%s returned %s twice or more' % \
                (method_id, movement.getRelativeUrl()))
      else:
        movement_set.add(movement)

    return movement_list
Romain Courteaud's avatar
Romain Courteaud committed
248 249 250

  def collectMovement(self, movement_list):
    """
251
      group movements in the way we want. Thanks to this method, we are able
Romain Courteaud's avatar
Romain Courteaud committed
252 253
      to retrieve movement classed by order, resource, criterion,....
      movement_list : the list of movement wich we want to group
254
      class_list : the list of classes used to group movements. The order
Romain Courteaud's avatar
Romain Courteaud committed
255 256 257 258 259
                   of the list is important and determines by what we will
                   group movement first
                   Typically, check_list is :
                   [DateMovementGroup,PathMovementGroup,...]
    """
260 261
    movement_group_list = self.getMovementGroupList()
    last_line_movement_group = self.getDeliveryMovementGroupList()[-1]
262
    separate_method_name_list = self.getDeliveryCellSeparateOrderList([])
263
    root_group_node = MovementGroupNode(
264 265 266
      separate_method_name_list=separate_method_name_list,
      movement_group_list=movement_group_list,
      last_line_movement_group=last_line_movement_group)
267 268
    root_group_node.append(movement_list)
    return root_group_node
Romain Courteaud's avatar
Romain Courteaud committed
269

270
  def _test(self, instance, movement_group_node_list,
271 272 273
                    divergence_list):
    result = True
    new_property_dict = {}
274 275
    for movement_group_node in movement_group_node_list:
      tmp_result, tmp_property_dict = movement_group_node.test(
276
        instance, divergence_list)
277
      if not tmp_result:
278 279 280 281
        result = tmp_result
      new_property_dict.update(tmp_property_dict)
    return result, new_property_dict

282
  def _findUpdatableObject(self, instance_list, movement_group_node_list,
283 284 285
                           divergence_list):
    instance = None
    property_dict = {}
Yoshinori Okuji's avatar
Yoshinori Okuji committed
286
    if not instance_list:
287
      for movement_group_node in movement_group_node_list:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
288
        for k, v in movement_group_node.getGroupEditDict().iteritems():
289 290 291 292
          if k in property_dict:
            raise DuplicatedPropertyDictKeysError(k)
          else:
            property_dict[k] = v
293
    else:
294 295
      # we want to check the original delivery first.
      # so sort instance_list by that current is exists or not.
296
      try:
297 298 299
        current = movement_group_node_list[-1].getMovementList()[0].getDeliveryValue()
        portal = self.getPortalObject()
        while current != portal:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
300 301 302 303 304 305
          try:
            instance_list.remove(current)
          except ValueError:
            pass
          else:
            instance_list.insert(0, current)
306 307
            break
          current = current.getParentValue()
308
      except AttributeError:
309
        pass
310
      for instance_to_update in instance_list:
311
        result, property_dict = self._test(
312
          instance_to_update, movement_group_node_list, divergence_list)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
313
        if result:
314
          instance = instance_to_update
Romain Courteaud's avatar
Romain Courteaud committed
315
          break
316
    return instance, property_dict
Romain Courteaud's avatar
Romain Courteaud committed
317

318 319 320
  @UnrestrictedMethod
  def buildDeliveryList(self, movement_group_node,
                        delivery_relative_url_list=None,
321
                        movement_list=None, update=True, **kw):
Romain Courteaud's avatar
Romain Courteaud committed
322 323 324
    """
      Build deliveries from a list of movements
    """
325 326 327
    # Parameter initialization
    if delivery_relative_url_list is None:
      delivery_relative_url_list = []
Jérome Perrin's avatar
Jérome Perrin committed
328 329
    if movement_list is None:
      movement_list = []
Romain Courteaud's avatar
Romain Courteaud committed
330
    # Module where we can create new deliveries
331 332
    portal = self.getPortalObject()
    delivery_module = getattr(portal, self.getDeliveryModule())
333 334 335 336 337 338 339
    if update:
      delivery_to_update_list = [portal.restrictedTraverse(relative_url) for \
                                 relative_url in delivery_relative_url_list]
      # Deliveries we are trying to update
      delivery_select_method_id = self.getDeliverySelectMethodId()
      if delivery_select_method_id not in ["", None]:
        to_update_delivery_sql_list = getattr(self, delivery_select_method_id) \
Romain Courteaud's avatar
Romain Courteaud committed
340
                                      (movement_list=movement_list)
341 342 343 344 345
        delivery_to_update_list.extend([sql_delivery.getObject() \
                                        for sql_delivery \
                                        in to_update_delivery_sql_list])
    else:
      delivery_to_update_list = []
346 347 348
    # We do not want to update the same object more than twice in one
    # _deliveryGroupProcessing().
    self._resetUpdated()
349
    delivery_list = self._processDeliveryGroup(
Romain Courteaud's avatar
Romain Courteaud committed
350
                          delivery_module,
351
                          movement_group_node,
352
                          self.getDeliveryMovementGroupList(),
353 354
                          delivery_to_update_list=delivery_to_update_list,
                          **kw)
Romain Courteaud's avatar
Romain Courteaud committed
355 356
    return delivery_list

357 358 359 360 361 362 363 364 365 366 367 368 369
  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

370 371 372 373 374
  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):
375 376 377
    """
      Build delivery from a list of movement
    """
378 379
    if movement_group_node_list is None:
      movement_group_node_list = []
380 381 382
    if divergence_list is None:
      divergence_list = []
    # do not use 'append' or '+=' because they are destructive.
383
    movement_group_node_list = movement_group_node_list + [movement_group_node]
384 385 386
    # Parameter initialization
    if delivery_to_update_list is None:
      delivery_to_update_list = []
Romain Courteaud's avatar
Romain Courteaud committed
387
    delivery_list = []
388 389

    if len(collect_order_list):
Romain Courteaud's avatar
Romain Courteaud committed
390
      # Get sorted movement for each delivery
391 392
      for grouped_node in movement_group_node.getGroupList():
        new_delivery_list = self._processDeliveryGroup(
Romain Courteaud's avatar
Romain Courteaud committed
393
                              delivery_module,
394
                              grouped_node,
Romain Courteaud's avatar
Romain Courteaud committed
395
                              collect_order_list[1:],
396
                              movement_group_node_list=movement_group_node_list,
397
                              delivery_to_update_list=delivery_to_update_list,
398 399
                              divergence_list=divergence_list,
                              activate_kw=activate_kw,
400
                              force_update=force_update)
Romain Courteaud's avatar
Romain Courteaud committed
401
        delivery_list.extend(new_delivery_list)
402
        force_update = 0
Romain Courteaud's avatar
Romain Courteaud committed
403
    else:
404
      # Test if we can update a existing delivery, or if we need to create
Romain Courteaud's avatar
Romain Courteaud committed
405
      # a new one
406 407 408 409 410
      delivery_to_update_list = [
        x for x in delivery_to_update_list \
        if x.getPortalType() == self.getDeliveryPortalType() and \
        not self._isUpdated(x, 'delivery')]
      delivery, property_dict = self._findUpdatableObject(
411
        delivery_to_update_list, movement_group_node_list,
412 413 414 415 416 417 418
        divergence_list)

      # if all deliveries are rejected in case of update, we update the
      # first one.
      if force_update and delivery is None and len(delivery_to_update_list):
        delivery = delivery_to_update_list[0]

Romain Courteaud's avatar
Romain Courteaud committed
419
      if delivery is None:
420 421 422
        delivery = self._createDelivery(delivery_module,
                                        movement_group_node.getMovementList(),
                                        activate_kw)
423 424 425
      # Put properties on delivery
      self._setUpdated(delivery, 'delivery')
      if property_dict:
426
        property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
Romain Courteaud's avatar
Romain Courteaud committed
427 428 429
        delivery.edit(**property_dict)

      # Then, create delivery line
430 431
      for grouped_node in movement_group_node.getGroupList():
        self._processDeliveryLineGroup(
Romain Courteaud's avatar
Romain Courteaud committed
432
                                delivery,
433
                                grouped_node,
434 435 436 437
                                self.getDeliveryLineMovementGroupList()[1:],
                                divergence_list=divergence_list,
                                activate_kw=activate_kw,
                                force_update=force_update)
Romain Courteaud's avatar
Romain Courteaud committed
438 439
      delivery_list.append(delivery)
    return delivery_list
440

441 442 443 444 445 446 447 448 449 450 451 452 453
  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

454 455 456 457
  def _processDeliveryLineGroup(self, delivery, movement_group_node,
                                collect_order_list, movement_group_node_list=None,
                                divergence_list=None,
                                activate_kw=None, force_update=0, **kw):
Romain Courteaud's avatar
Romain Courteaud committed
458 459 460
    """
      Build delivery line from a list of movement on a delivery
    """
461 462
    if movement_group_node_list is None:
      movement_group_node_list = []
463 464 465
    if divergence_list is None:
      divergence_list = []
    # do not use 'append' or '+=' because they are destructive.
466
    movement_group_node_list = movement_group_node_list + [movement_group_node]
467

468
    if len(collect_order_list) and not movement_group_node.getCurrentMovementGroup().isBranch():
Romain Courteaud's avatar
Romain Courteaud committed
469
      # Get sorted movement for each delivery line
470 471 472 473 474 475
      for grouped_node in movement_group_node.getGroupList():
        self._processDeliveryLineGroup(
          delivery,
          grouped_node,
          collect_order_list[1:],
          movement_group_node_list=movement_group_node_list,
476
          divergence_list=divergence_list,
477 478
          activate_kw=activate_kw,
          force_update=force_update)
Romain Courteaud's avatar
Romain Courteaud committed
479 480 481
    else:
      # Test if we can update an existing line, or if we need to create a new
      # one
482 483 484 485
      delivery_line_to_update_list = [x for x in delivery.contentValues(
        portal_type=self.getDeliveryLinePortalType()) if \
                                      not self._isUpdated(x, 'line')]
      delivery_line, property_dict = self._findUpdatableObject(
486
        delivery_line_to_update_list, movement_group_node_list,
487 488 489 490
        divergence_list)
      if delivery_line is not None:
        update_existing_line = 1
      else:
Romain Courteaud's avatar
Romain Courteaud committed
491
        # Create delivery line
492
        update_existing_line = 0
493 494 495 496
        delivery_line = self._createDeliveryLine(
                delivery,
                movement_group_node.getMovementList(),
                activate_kw)
497 498 499
      # Put properties on delivery line
      self._setUpdated(delivery_line, 'line')
      if property_dict:
500
        property_dict.setdefault('edit_order', ('stop_date', 'start_date'))
501
        delivery_line.edit(force_update=1, **property_dict)
502

503 504 505 506 507 508 509 510 511 512 513 514
      if movement_group_node.getCurrentMovementGroup().isBranch():
        for grouped_node in movement_group_node.getGroupList():
          self._processDeliveryLineGroup(
            delivery_line,
            grouped_node,
            collect_order_list[1:],
            movement_group_node_list=movement_group_node_list,
            divergence_list=divergence_list,
            activate_kw=activate_kw,
            force_update=force_update)
        return

Romain Courteaud's avatar
Romain Courteaud committed
515
      # Update variation category list on line
516 517 518
      variation_category_dict = dict([(variation_category, True) for
                                      variation_category in
                                      delivery_line.getVariationCategoryList()])
519
      for movement in movement_group_node.getMovementList():
520 521 522 523
        for category in movement.getVariationCategoryList():
          variation_category_dict[category] = True
      variation_category_list = sorted(variation_category_dict.keys())
      delivery_line.setVariationCategoryList(variation_category_list)
Romain Courteaud's avatar
Romain Courteaud committed
524 525
      # Then, create delivery movement (delivery cell or complete delivery
      # line)
526
      grouped_node_list = movement_group_node.getGroupList()
527
      # If no group is defined for cell, we need to continue, in order to
528
      # save the quantity value
529 530 531
      if len(grouped_node_list):
        for grouped_node in grouped_node_list:
          self._processDeliveryCellGroup(
Romain Courteaud's avatar
Romain Courteaud committed
532
                                    delivery_line,
533
                                    grouped_node,
534
                                    self.getDeliveryCellMovementGroupList()[1:],
535
                                    update_existing_line=update_existing_line,
536 537 538
                                    divergence_list=divergence_list,
                                    activate_kw=activate_kw,
                                    force_update=force_update)
539
      else:
540
        self._processDeliveryCellGroup(
541
                                  delivery_line,
542
                                  movement_group_node,
543
                                  [],
544
                                  update_existing_line=update_existing_line,
545 546 547
                                  divergence_list=divergence_list,
                                  activate_kw=activate_kw,
                                  force_update=force_update)
548

549 550 551 552 553 554 555 556 557 558 559
  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
Romain Courteaud's avatar
Romain Courteaud committed
560

561 562 563 564 565
  def _processDeliveryCellGroup(self, delivery_line, movement_group_node,
                                collect_order_list, movement_group_node_list=None,
                                update_existing_line=0,
                                divergence_list=None,
                                activate_kw=None, force_update=0):
Romain Courteaud's avatar
Romain Courteaud committed
566 567 568 569
    """
      Build delivery cell from a list of movement on a delivery line
      or complete delivery line
    """
570 571
    if movement_group_node_list is None:
      movement_group_node_list = []
572 573 574
    if divergence_list is None:
      divergence_list = []
    # do not use 'append' or '+=' because they are destructive.
575
    movement_group_node_list = movement_group_node_list + [movement_group_node]
576 577

    if len(collect_order_list):
Romain Courteaud's avatar
Romain Courteaud committed
578
      # Get sorted movement for each delivery line
579 580
      for grouped_node in movement_group_node.getGroupList():
        self._processDeliveryCellGroup(
581
          delivery_line,
582
          grouped_node,
583
          collect_order_list[1:],
584
          movement_group_node_list=movement_group_node_list,
585 586 587 588
          update_existing_line=update_existing_line,
          divergence_list=divergence_list,
          activate_kw=activate_kw,
          force_update=force_update)
Romain Courteaud's avatar
Romain Courteaud committed
589
    else:
590
      movement_list = movement_group_node.getMovementList()
Romain Courteaud's avatar
Romain Courteaud committed
591
      if len(movement_list) != 1:
592
        raise CollectError, "DeliveryBuilder: %s unable to distinct those\
Romain Courteaud's avatar
Romain Courteaud committed
593 594 595 596 597 598
              movements: %s" % (self.getId(), str(movement_list))
      else:
        # XXX Hardcoded value
        base_id = 'movement'
        object_to_update = None
        # We need to initialize the cell
599
        update_existing_movement = 0
Romain Courteaud's avatar
Romain Courteaud committed
600 601 602 603 604
        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
605
        property_dict = {}
606
        if len(delivery_line.getCellKeyList(base_id=base_id)) == 0:
Romain Courteaud's avatar
Romain Courteaud committed
607
          # update line
608 609 610 611 612
          if update_existing_line == 1:
            if self._isUpdated(delivery_line, 'cell'):
              object_to_update_list = []
            else:
              object_to_update_list = [delivery_line]
613 614 615
          else:
            object_to_update_list = []
          object_to_update, property_dict = self._findUpdatableObject(
616
            object_to_update_list, movement_group_node_list,
617
            divergence_list)
618 619 620 621
          if object_to_update is not None:
            update_existing_movement = 1
          else:
            object_to_update = delivery_line
Romain Courteaud's avatar
Romain Courteaud committed
622
        else:
623 624 625 626 627
          object_to_update_list = [
            delivery_line.getCell(base_id=base_id, *cell_key) for cell_key in \
            delivery_line.getCellKeyList(base_id=base_id) \
            if delivery_line.hasCell(base_id=base_id, *cell_key)]
          object_to_update, property_dict = self._findUpdatableObject(
628
            object_to_update_list, movement_group_node_list,
629 630 631 632 633 634 635
            divergence_list)
          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

Romain Courteaud's avatar
Romain Courteaud committed
636 637
        if object_to_update is None:
          # create a new cell
638
          cell_key = movement.getVariationCategoryList(
639
              omit_optional_variation=1)
Romain Courteaud's avatar
Romain Courteaud committed
640
          if not delivery_line.hasCell(base_id=base_id, *cell_key):
641 642
            cell = self._createDeliveryCell(delivery_line, movement,
                                            activate_kw, base_id, cell_key)
643 644
            vcl = movement.getVariationCategoryList()
            cell._edit(category_list=vcl,
645 646 647 648
                       # XXX hardcoded value
                       mapped_value_property_list=('quantity', 'price'),
                       membership_criterion_category_list=vcl,
                       membership_criterion_base_category_list=movement.\
Romain Courteaud's avatar
Romain Courteaud committed
649 650
                                             getVariationBaseCategoryList())
          else:
651
            raise MatrixError, 'Cell: %s already exists on %s' % \
Romain Courteaud's avatar
Romain Courteaud committed
652
                  (str(cell_key), str(delivery_line))
653
          object_to_update = cell
654
        self._setUpdated(object_to_update, 'cell')
Romain Courteaud's avatar
Romain Courteaud committed
655 656
        self._setDeliveryMovementProperties(
                            object_to_update, movement, property_dict,
657
                            update_existing_movement=update_existing_movement,
658
                            force_update=force_update, activate_kw=activate_kw)
Romain Courteaud's avatar
Romain Courteaud committed
659 660 661

  def _setDeliveryMovementProperties(self, delivery_movement,
                                     simulation_movement, property_dict,
662
                                     update_existing_movement=0,
663
                                     force_update=0, activate_kw=None):
Romain Courteaud's avatar
Romain Courteaud committed
664 665 666
    """
      Initialize or update delivery movement properties.
    """
667
    if not update_existing_movement or force_update:
Romain Courteaud's avatar
Romain Courteaud committed
668 669
      # Now, only 1 movement is possible, so copy from this movement
      # XXX hardcoded value
670 671 672 673
      if getattr(simulation_movement, 'getMappedProperty', None) is not None:
        property_dict['quantity'] = simulation_movement.getMappedProperty('quantity')
      else:
        property_dict['quantity'] = simulation_movement.getQuantity()
Romain Courteaud's avatar
Romain Courteaud committed
674 675
      property_dict['price'] = simulation_movement.getPrice()
      # Update properties on object (quantity, price...)
676
      delivery_movement._edit(force_update=1, **property_dict)
677

678 679
  @UnrestrictedMethod
  def callAfterBuildingScript(self, delivery_list, movement_list=None, **kw):
680 681
    """
      Call script on each delivery built.
682
    """
683 684
    if not len(delivery_list):
      return
Jérome Perrin's avatar
Jérome Perrin committed
685 686 687
    # Parameter initialization
    if movement_list is None:
      movement_list = []
688 689
    delivery_after_generation_script_id = \
                              self.getDeliveryAfterGenerationScriptId()
690 691
    related_simulation_movement_path_list = \
                              [x.getPath() for x in movement_list]
692 693
    if delivery_after_generation_script_id not in ["", None]:
      for delivery in delivery_list:
694
        script = getattr(delivery, delivery_after_generation_script_id)
695 696 697 698 699
        # 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
700 701 702
        meta_type = getattr(script, 'meta_type', None)
        if meta_type == 'Script (Python)':
          # check if the script accepts related_simulation_movement_path_list
703
          safe_to_pass_parameter = False
704 705
          for param in script.params().split(','):
            param = param.split('=', 1)[0].strip()
706 707 708
            if param == 'related_simulation_movement_path_list' \
                    or param.startswith('**'):
              safe_to_pass_parameter = True
709
              break
710 711

        if safe_to_pass_parameter:
712
          script(related_simulation_movement_path_list=related_simulation_movement_path_list)
713 714
        else:
          script()
715 716 717 718 719 720 721 722

  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
723
    portal = self.getPortalObject()
724
    if portal_type is None:
Yoshinori Okuji's avatar
Yoshinori Okuji committed
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
      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)
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 776 777 778 779 780 781 782 783 784 785 786 787 788

  # 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')

  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

  def _isUpdated(self, obj, level):
    tv = getTransactionalVariable(self)
    return level in tv['builder_processed_list'].get(obj, [])

  def _setUpdated(self, obj, level):
    tv = getTransactionalVariable(self)
    if tv.get('builder_processed_list', None) is None:
      self._resetUpdated()
    tv['builder_processed_list'][obj] = \
       tv['builder_processed_list'].get(obj, []) + [level]

  def _resetUpdated(self):
    tv = getTransactionalVariable(self)
    tv['builder_processed_list'] = {}
789 790

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