DeliveryBuilder.py 13 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
31
from Products.ERP5.Document.OrderBuilder import OrderBuilder
32
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Romain Courteaud's avatar
Romain Courteaud committed
33

34 35 36
class SelectMethodError(Exception): pass
class SelectMovementError(Exception): pass

37
class DeliveryBuilder(OrderBuilder):
Romain Courteaud's avatar
Romain Courteaud committed
38 39
  """
    Delivery Builder objects allow to gather multiple Simulation Movements
40
    into a single Delivery.
Romain Courteaud's avatar
Romain Courteaud committed
41 42 43 44 45

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

    Delivery Builders are called for example whenever an order is confirmed.
46 47
    They are also called globaly in order to gather any confirmed or above
    Simulation Movement which was not associated to any Delivery Line.
Romain Courteaud's avatar
Romain Courteaud committed
48 49
    Such movements are called orphaned Simulation Movements.

50
    Delivery Builder objects are provided with a set a parameters to achieve
Romain Courteaud's avatar
Romain Courteaud committed
51 52
    their goal:

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

56 57
    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
58 59
    definition).

60
    collect_order_list which defines how to group selected movements
Romain Courteaud's avatar
Romain Courteaud committed
61 62
    according to gathering rules.

63
    delivery_select_method which defines how to select existing Delivery
Romain Courteaud's avatar
Romain Courteaud committed
64 65
    which may eventually be updated with selected simulation movements.

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

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

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

  # Declarative security
  security = ClassSecurityInfo()
81
  security.declareObjectProtected(Permissions.AccessContentsInformation)
Romain Courteaud's avatar
Romain Courteaud committed
82 83 84 85 86 87 88 89 90 91 92 93

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

94 95 96 97 98 99
  def callBeforeBuildingScript(self):
    """
      Redefine this method, because it seems nothing interesting can be
      done before building Delivery.
    """
    pass
100

101 102
  @UnrestrictedMethod
  def searchMovementList(self, applied_rule_uid=None, **kw):
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
    """
      defines how to query all Simulation Movements which meet certain criteria
      (including the above path path definition).

      First, select movement matching to criteria define on DeliveryBuilder
      Then, call script simulation_select_method to restrict movement_list
    """
    movement_list = []
    # We only search Simulation Movement
    kw['portal_type'] = 'Simulation Movement'
    # Search only child movement from this applied rule
    if applied_rule_uid is not None:
      kw['parent_uid'] = applied_rule_uid
    # XXX Add profile query
    # Add resource query
    if self.getResourcePortalType() not in ('', None):
      kw['resourceType'] = self.getResourcePortalType()
120
    if self.getSimulationSelectMethodId() in ['', None]:
121 122
      movement_list = [x.getObject() for x in self.portal_catalog(**kw)]
    else:
123
      select_method = getattr(self.getPortalObject(), self.getSimulationSelectMethodId())
124 125
      movement_list = select_method(**kw)
    # XXX Use buildSQLQuery will be better
Romain Courteaud's avatar
Romain Courteaud committed
126
    movement_list = [x for x in movement_list if \
127
                     x.getDeliveryValueList()==[]]
128
    # XXX  Add predicate test
129 130 131 132 133 134
    # XXX FIXME Check that there is no double in the list
    # Because we can't trust simulation_select_method
    # Example: simulation_select_method is not tested enough
    mvt_dict = {}
    for movement in movement_list:
      if mvt_dict.has_key(movement):
135
        raise SelectMethodError, \
136
              "%s return %s twice (or more)" % \
137
              (str(self.getSimulationSelectMethodId()),
138 139 140 141
               str(movement.getRelativeUrl()))
      else:
        mvt_dict[movement] = 1
    # Return result
142 143
    return movement_list

144 145
  def _setDeliveryMovementProperties(self, delivery_movement,
                                     simulation_movement, property_dict,
146
                                     update_existing_movement=0,
147
                                     force_update=0, activate_kw=None):
148 149 150 151 152 153
    """
      Initialize or update delivery movement properties.
      Set delivery ratio on simulation movement.
      Create the relation between simulation movement
      and delivery movement.
    """
154
    OrderBuilder._setDeliveryMovementProperties(
155
                            self, delivery_movement,
156
                            simulation_movement, property_dict,
157
                            update_existing_movement=update_existing_movement,
158 159 160 161
                            force_update=force_update, 
                            activate_kw=activate_kw)
    simulation_movement.edit(delivery_value=delivery_movement,
                             activate_kw=activate_kw)
Romain Courteaud's avatar
Romain Courteaud committed
162

163
  # Simulation consistency propagation
164
  security.declareProtected(Permissions.ModifyPortalContent,
165
                            'updateFromSimulation')
166
  def updateFromSimulation(self, delivery_relative_url, **kw):
167
    """
168
      Update all lines of this transaction based on movements in the
169 170
      simulation related to this transaction.
    """
171 172 173 174 175 176 177 178
    # We have to get a delivery, else, raise a Error
    delivery = self.getPortalObject().restrictedTraverse(delivery_relative_url)

    divergence_to_adopt_list = delivery.getDivergenceList()
    return self.solveDivergence(
      delivery_relative_url,
      divergence_to_adopt_list=divergence_to_adopt_list)

179 180 181
  @UnrestrictedMethod
  def solveDeliveryGroupDivergence(self, delivery_relative_url,
                                   property_dict=None):
182 183 184 185 186 187 188
    """
      solve each divergence according to users decision (accept, adopt
      or do nothing).
    """
    if property_dict in (None, {}):
      return
    delivery = self.getPortalObject().restrictedTraverse(delivery_relative_url)
189 190
    for (property, value) in property_dict.iteritems():
      delivery.setPropertyList(property, value)
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
    # Try to remove existing properties/categories from Movements that
    # should exist on Deliveries.
    for movement in delivery.getMovementList():
      for prop in property_dict.keys():
        # XXX The following should be implemented in better way.
        if movement.hasProperty(prop):
          try:
            # for Property
            movement._delProperty(prop)
          except AttributeError:
            # for Category
            movement.setProperty(prop, None)

    divergence_to_accept_list = []
    for divergence in delivery.getDivergenceList():
      if divergence.getProperty('tested_property') not in property_dict.keys():
        continue
      divergence_to_accept_list.append(divergence)
    self._solveDivergence(delivery_relative_url,
                          divergence_to_accept_list=divergence_to_accept_list)

  def _solveDivergence(self, delivery_relative_url,
                       divergence_to_accept_list=None,
                       divergence_to_adopt_list=None,
                       **kw):
217 218 219 220
    """
      solve each divergence according to users decision (accept, adopt
      or do nothing).
    """
221
    # We have to get a delivery, else, raise a Error
222
    delivery = self.getPortalObject().restrictedTraverse(delivery_relative_url)
223

224 225 226 227 228 229 230 231
    if divergence_to_accept_list is None:
      divergence_to_accept_list = []
    if divergence_to_adopt_list is None:
      divergence_to_adopt_list = []

    if not len(divergence_to_accept_list) and \
           not len(divergence_to_adopt_list):
      return
232
    divergence_list = delivery.getDivergenceList()
233 234 235 236 237 238 239 240 241 242 243 244

    # First, we update simulation movements according to
    # divergence_to_accept_list.
    if len(divergence_to_accept_list):
      solver_script = delivery._getTypeBasedMethod('acceptDecision',
                                                   'Delivery_acceptDecision')
      solver_script(divergence_to_accept_list)

    # Then, we update delivery/line/cell from simulation movements
    # according to divergence_to_adopt_list.
    if not len(divergence_to_adopt_list):
      return
245 246

    # Select
247 248 249
    movement_type_list = (self.getDeliveryLinePortalType(),
            self.getDeliveryCellPortalType())
    movement_list = delivery.getMovementList(portal_type=movement_type_list)
250
    simulation_movement_list = []
251
    for movement in movement_list:
252
      movement.edit(quantity=0)
253 254
      for simulation_movement in movement.getDeliveryRelatedValueList(
                                            portal_type="Simulation Movement"):
255
        simulation_movement_list.append(simulation_movement)
256 257

    # Collect
258
    root_group_node = self.collectMovement(simulation_movement_list)
259

260 261 262 263 264
    # Build
    portal = self.getPortalObject()
    delivery_module = getattr(portal, self.getDeliveryModule())
    delivery_to_update_list = [delivery]
    self._resetUpdated()
265
    delivery_list = self._processDeliveryGroup(
266
      delivery_module,
267
      root_group_node,
268 269 270 271 272 273 274 275 276
      self.getDeliveryMovementGroupList(),
      delivery_to_update_list=delivery_to_update_list,
      divergence_list=divergence_to_adopt_list,
      force_update=1)

    # Then, we should re-apply quantity divergence according to 'Do
    # nothing' quanity divergence list because all quantity are already
    # calculated in adopt prevision phase.
    quantity_dict = {}
277
    for divergence in divergence_list:
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
      if divergence.getProperty('divergence_scope') != 'quantity' or \
             divergence in divergence_to_accept_list or \
             divergence in divergence_to_adopt_list:
        continue
      s_m = divergence.getProperty('simulation_movement')
      delivery_movement = s_m.getDeliveryValue()
      quantity_gap = divergence.getProperty('decision_value') - \
                     divergence.getProperty('prevision_value')
      delivery_movement.setQuantity(delivery_movement.getQuantity() + \
                                    quantity_gap)
      quantity_dict[s_m] = \
          divergence.getProperty('decision_value')

    # Finally, recalculate delivery_ratio
    #
    # Here, created/updated movements are not indexed yet. So we try to
    # gather delivery relations from simulation movements.
    delivery_dict = {}
    for s_m in simulation_movement_list:
      delivery_path = s_m.getDelivery()
      delivery_dict[delivery_path] = \
                                   delivery_dict.get(delivery_path, []) + \
                                   [s_m]

    for s_m_list_per_movement in delivery_dict.values():
303 304
      total_quantity = sum([quantity_dict.get(s_m, s_m.getQuantity()) \
                            for s_m in s_m_list_per_movement])
305 306 307 308 309 310 311 312 313
      if total_quantity != 0.0:
        for s_m in s_m_list_per_movement:
          delivery_ratio = quantity_dict.get(s_m, s_m.getQuantity()) \
                                             / total_quantity
          s_m.edit(delivery_ratio=delivery_ratio)
      else:
        for s_m in s_m_list_per_movement:
          delivery_ratio = 1.0 / len(s_m_list_per_movement)
          s_m.edit(delivery_ratio=delivery_ratio)
314

315 316 317
    # Call afterscript if new deliveries are created
    new_delivery_list = [x for x in delivery_list if x != delivery]
    self.callAfterBuildingScript(new_delivery_list, simulation_movement_list)
318

319
    return delivery_list
320 321

  solveDivergence = UnrestrictedMethod(_solveDivergence)