From bdf1a31d08b011ad402c36a1ded5509f691b2f85 Mon Sep 17 00:00:00 2001 From: Jean-Paul Smets <jp@nexedi.com> Date: Wed, 4 Aug 2010 19:18:59 +0000 Subject: [PATCH] New name for builder classes git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@37488 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/DeliveryBuilder.py | 780 ++++++++++++++++++ .../ERP5/Document/SimulatedDeliveryBuilder.py | 436 ++++++++++ 2 files changed, 1216 insertions(+) create mode 100644 product/ERP5/Document/DeliveryBuilder.py create mode 100644 product/ERP5/Document/SimulatedDeliveryBuilder.py diff --git a/product/ERP5/Document/DeliveryBuilder.py b/product/ERP5/Document/DeliveryBuilder.py new file mode 100644 index 0000000000..b228ecda5b --- /dev/null +++ b/product/ERP5/Document/DeliveryBuilder.py @@ -0,0 +1,780 @@ +# -*- 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 +from Products.ERP5Type.XMLObject import XMLObject +from Products.ERP5.Document.Predicate import Predicate +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 +from Acquisition import aq_parent, aq_inner + +from zLOG import LOG + +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 + ) + + 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 movement_relative_url_list is None: + movement_relative_url_list = [] + if delivery_relative_url_list is None: + delivery_relative_url_list = [] + if movement_list is None: + movement_list = [] + # Call a script before building + self.callBeforeBuildingScript() # XXX-JPS Used ? + # Select + if not len(movement_list): + if len(movement_relative_url_list) == 0: + movement_list = self.searchMovementList( + delivery_relative_url_list=delivery_relative_url_list, + applied_rule_uid=applied_rule_uid,**kw) + else: + movement_list = [self.restrictedTraverse(relative_url) for relative_url \ + in movement_relative_url_list] + LOG('movement_list', 0, repr(movement_list)) + if not len(movement_list): + 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, + movement_list=movement_list,**kw) + # 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() + if delivery_module_before_building_script_id not in ["", None]: + 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 + + @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 + + 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() + last_line_movement_group = self.getDeliveryMovementGroupList()[-1] + 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 + + def _findUpdatableObject(self, instance_list, movement_group_node_list, + divergence_list): + instance = None + property_dict = {} + if not len(instance_list): + for movement_group_node in movement_group_node_list: + for k,v in movement_group_node.getGroupEditDict().iteritems(): + if k in property_dict: + raise DuplicatedPropertyDictKeysError(k) + else: + property_dict[k] = v + else: + # we want to check the original delivery first. + # so sort instance_list by that current is exists or not. + try: + current = movement_group_node_list[-1].getMovementList()[0].getDeliveryValue() + portal = self.getPortalObject() + while current != portal: + if current in instance_list: + instance_list.sort(key=lambda x: x != current and 1 or 0) + break + current = current.getParentValue() + except AttributeError: + pass + for instance_to_update in instance_list: + result, property_dict = self._test( + instance_to_update, movement_group_node_list, divergence_list) + if result == True: + 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: + 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) \ + (movement_list=movement_list) + delivery_to_update_list.extend([sql_delivery.getObject() \ + for sql_delivery \ + in to_update_delivery_sql_list]) + else: + delivery_to_update_list = [] + # We do not want to update the same object more than twice in one + # _deliveryGroupProcessing(). + self._resetUpdated() + 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 = [] + # do not use 'append' or '+=' because they are destructive. + movement_group_node_list = movement_group_node_list + [movement_group_node] + # Parameter initialization + if delivery_to_update_list is None: + delivery_to_update_list = [] + delivery_list = [] + + if len(collect_order_list): + # Get sorted movement for each delivery + for grouped_node in movement_group_node.getGroupList(): + new_delivery_list = self._processDeliveryGroup( + delivery_module, + grouped_node, + collect_order_list[1:], + movement_group_node_list=movement_group_node_list, + delivery_to_update_list=delivery_to_update_list, + divergence_list=divergence_list, + activate_kw=activate_kw, + force_update=force_update) + 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_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( + delivery_to_update_list, movement_group_node_list, + 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] + + if delivery is None: + delivery = self._createDelivery(delivery_module, + movement_group_node.getMovementList(), + activate_kw) + # Put properties on delivery + self._setUpdated(delivery, 'delivery') + if property_dict: + property_dict.setdefault('edit_order', ('stop_date', 'start_date')) + delivery.edit(**property_dict) + + # Then, create delivery line + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryLineGroup( + delivery, + grouped_node, + self.getDeliveryLineMovementGroupList()[1:], + divergence_list=divergence_list, + 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, + 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 = [] + # do not use 'append' or '+=' because they are destructive. + movement_group_node_list = movement_group_node_list + [movement_group_node] + + if len(collect_order_list) and not movement_group_node.getCurrentMovementGroup().isBranch(): + # Get sorted movement for each delivery line + 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, + divergence_list=divergence_list, + 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_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( + delivery_line_to_update_list, movement_group_node_list, + divergence_list) + if delivery_line is not None: + update_existing_line = 1 + else: + # Create delivery line + update_existing_line = 0 + delivery_line = self._createDeliveryLine( + delivery, + movement_group_node.getMovementList(), + activate_kw) + # Put properties on delivery line + self._setUpdated(delivery_line, 'line') + if property_dict: + property_dict.setdefault('edit_order', ('stop_date', 'start_date')) + delivery_line.edit(force_update=1, **property_dict) + + 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 + + # Update variation category list on line + variation_category_dict = dict([(variation_category, True) for + variation_category in + delivery_line.getVariationCategoryList()]) + for movement in movement_group_node.getMovementList(): + for category in movement.getVariationCategoryList(): + variation_category_dict[category] = True + variation_category_list = sorted(variation_category_dict.keys()) + delivery_line.setVariationCategoryList(variation_category_list) + # 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 + if len(grouped_node_list): + for grouped_node in grouped_node_list: + self._processDeliveryCellGroup( + delivery_line, + grouped_node, + self.getDeliveryCellMovementGroupList()[1:], + update_existing_line=update_existing_line, + divergence_list=divergence_list, + activate_kw=activate_kw, + force_update=force_update) + else: + self._processDeliveryCellGroup( + delivery_line, + movement_group_node, + [], + update_existing_line=update_existing_line, + divergence_list=divergence_list, + 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, + 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 = [] + if divergence_list is None: + divergence_list = [] + # do not use 'append' or '+=' because they are destructive. + movement_group_node_list = movement_group_node_list + [movement_group_node] + + if len(collect_order_list): + # Get sorted movement for each delivery line + for grouped_node in movement_group_node.getGroupList(): + self._processDeliveryCellGroup( + delivery_line, + grouped_node, + collect_order_list[1:], + movement_group_node_list=movement_group_node_list, + update_existing_line=update_existing_line, + divergence_list=divergence_list, + 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: + # XXX Hardcoded value + base_id = 'movement' + 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 = {} + if len(delivery_line.getCellKeyList(base_id=base_id)) == 0: + # update line + if update_existing_line == 1: + if self._isUpdated(delivery_line, 'cell'): + object_to_update_list = [] + else: + object_to_update_list = [delivery_line] + else: + object_to_update_list = [] + object_to_update, property_dict = self._findUpdatableObject( + object_to_update_list, movement_group_node_list, + divergence_list) + if object_to_update is not None: + update_existing_movement = 1 + else: + object_to_update = delivery_line + else: + 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( + object_to_update_list, movement_group_node_list, + 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 + + if object_to_update is None: + # 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() + 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 + self._setUpdated(object_to_update, 'cell') + 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...) + delivery_movement._edit(force_update=1, **property_dict) + + @UnrestrictedMethod + def callAfterBuildingScript(self, delivery_list, movement_list=None, **kw): + """ + Call script on each delivery built. + """ + if not len(delivery_list): + 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] + if delivery_after_generation_script_id not in ["", None]: + 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. + """ + category_index_dict = {} + for i in self.getPortalObject().portal_categories.collect_order_group.contentValues(): + category_index_dict[i.getId()] = i.getIntIndex() + + def sort_movement_group(a, b): + return cmp(category_index_dict.get(a.getCollectOrderGroup()), + category_index_dict.get(b.getCollectOrderGroup())) or \ + cmp(a.getIntIndex(), b.getIntIndex()) + if portal_type is None: + portal_type = self.getPortalMovementGroupTypeList() + movement_group_list = [x for x in self.contentValues(filter={'portal_type': portal_type}) \ + if collect_order_group is None or collect_order_group == x.getCollectOrderGroup()] + return sorted(movement_group_list, sort_movement_group) + + # 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'] = {} + + # for backward compatibilities. + _deliveryGroupProcessing = _processDeliveryGroup + _deliveryLineGroupProcessing = _processDeliveryLineGroup + _deliveryCellGroupProcessing = _processDeliveryCellGroup diff --git a/product/ERP5/Document/SimulatedDeliveryBuilder.py b/product/ERP5/Document/SimulatedDeliveryBuilder.py new file mode 100644 index 0000000000..9fbc870115 --- /dev/null +++ b/product/ERP5/Document/SimulatedDeliveryBuilder.py @@ -0,0 +1,436 @@ +# -*- 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 +from Products.ERP5.Document.OrderBuilder import OrderBuilder, \ + SelectMethodError +from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod +from Products.ERP5Type.CopySupport import CopyError, tryMethodCallWithTemporaryPermission + +class DeliveryBuilder(OrderBuilder): + """ + Delivery 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. + + Delivery Builders are called for example whenever an order is confirmed. + They are also called globaly in order to gather any confirmed or above + Simulation Movement which was not associated to any Delivery Line. + Such movements are called orphaned Simulation Movements. + + Delivery 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. + + 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 + Deliveries. + """ + + # CMF Type Definition + meta_type = 'ERP5 Delivery Builder' + portal_type = 'Delivery 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 + ) + + def callBeforeBuildingScript(self): # XXX-JPS + """ + Redefine this method, because it seems nothing interesting can be + done before building Delivery. + """ + pass + + @UnrestrictedMethod + def searchMovementList(self, applied_rule_uid=None, **kw): + """ + 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() + if self.getSimulationSelectMethodId() in ['', None]: + movement_list = [x.getObject() for x in self.portal_catalog(**kw)] + else: + select_method = getattr(self.getPortalObject(), self.getSimulationSelectMethodId()) + movement_list = select_method(**kw) + # XXX Use buildSQLQuery will be better + movement_list = [x for x in movement_list if \ + x.getDeliveryValueList()==[]] + # XXX Add predicate test + # 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): + raise SelectMethodError, \ + "%s return %s twice (or more)" % \ + (str(self.getSimulationSelectMethodId()), + str(movement.getRelativeUrl())) + else: + mvt_dict[movement] = 1 + # Return result + return movement_list + + 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. + Set delivery ratio on simulation movement. + Create the relation between simulation movement + and delivery movement. + """ + OrderBuilder._setDeliveryMovementProperties( + self, delivery_movement, + simulation_movement, property_dict, + update_existing_movement=update_existing_movement, + force_update=force_update, + activate_kw=activate_kw) + + if update_existing_movement and not force_update: + # Important. + # Attributes of delivery_movement must not be modified here. + # Because we can not change values modified by the user. + # Delivery will probably diverge now, but this is not the job of + # DeliveryBuilder to resolve such problem. + # Use Solver instead. + simulation_movement.edit(delivery_ratio=0) + else: + simulation_movement.edit(delivery_ratio=1) + + simulation_movement.edit(delivery_value=delivery_movement, + activate_kw=activate_kw) + + # Simulation consistency propagation + security.declareProtected(Permissions.ModifyPortalContent, + 'updateFromSimulation') + def updateFromSimulation(self, delivery_relative_url, **kw): + """ + Update all lines of this transaction based on movements in the + simulation related to this transaction. + """ + # 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) + + @UnrestrictedMethod + def solveDeliveryGroupDivergence(self, delivery_relative_url, + property_dict=None): + """ + 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) + for (property, value) in property_dict.iteritems(): + delivery.setPropertyList(property, value) + + # 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, # XXX-JPS what is this doing here ????? + divergence_to_accept_list=None, + divergence_to_adopt_list=None, + **kw): + """ + solve each divergence according to users decision (accept, adopt + or do nothing). + """ + # We have to get a delivery, else, raise a Error + delivery = self.getPortalObject().restrictedTraverse(delivery_relative_url) + + 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 + divergence_list = delivery.getDivergenceList() + + # 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 + + # Select + movement_type_list = (self.getDeliveryLinePortalType(), + self.getDeliveryCellPortalType()) + movement_list = delivery.getMovementList(portal_type=movement_type_list) + simulation_movement_list = [] + for movement in movement_list: + movement.edit(quantity=0) + for simulation_movement in movement.getDeliveryRelatedValueList( + portal_type="Simulation Movement"): + simulation_movement_list.append(simulation_movement) + + # Collect + root_group_node = self.collectMovement(simulation_movement_list) + + # Build + portal = self.getPortalObject() + delivery_module = getattr(portal, self.getDeliveryModule()) + delivery_to_update_list = [delivery] + self._resetUpdated() + delivery_list = self._processDeliveryGroup( + delivery_module, + root_group_node, + 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 = {} + for divergence in divergence_list: + 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(): + total_quantity = sum([quantity_dict.get(s_m, + s_m.getMappedProperty('quantity')) \ + for s_m in s_m_list_per_movement]) + if total_quantity != 0.0: + for s_m in s_m_list_per_movement: + delivery_ratio = quantity_dict.get(s_m, + s_m.getMappedProperty('quantity')) \ + / 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) + + # 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) + + return delivery_list + + solveDivergence = UnrestrictedMethod(_solveDivergence) + + def _createDelivery(self, delivery_module, movement_list, activate_kw): + """ + Refer to the docstring in OrderBuilder. + Unlike OrderBuilder, DeliveryBuilder needs to respect + existing relationship. + """ + try: + old_delivery = self._searchUpByPortalType( + movement_list[0].getDeliveryValue(), + self.getDeliveryPortalType()) + except AttributeError: + old_delivery = None + if old_delivery is None: + # from scratch + 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) + else: + # from duplicated original delivery + cp = tryMethodCallWithTemporaryPermission( + delivery_module, 'Copy or Move', + lambda parent, *ids: + parent._duplicate(parent.manage_copyObjects(ids=ids))[0], + (delivery_module, old_delivery.getId()), {}, CopyError) + delivery = delivery_module[cp['new_id']] + # delete non-split movements + keep_id_list = [y.getDeliveryValue().getId() for y in movement_list] + delete_id_list = [x.getId() for x in delivery.contentValues() \ + if x.getId() not in keep_id_list] + delivery.deleteContent(delete_id_list) + + return delivery + + def _createDeliveryLine(self, delivery, movement_list, activate_kw): + """ + Refer to the docstring in OrderBuilder. + Unlike OrderBuilder, DeliveryBuilder needs to respect + existing relationship. + """ + try: + old_delivery_line = self._searchUpByPortalType( + movement_list[0].getDeliveryValue(), + self.getDeliveryLinePortalType()) + except AttributeError: + old_delivery_line = None + if old_delivery_line is None: + # from scratch + new_delivery_line_id = str(delivery.generateNewId()) + delivery_line = delivery.newContent( + portal_type=self.getDeliveryLinePortalType(), + id=new_delivery_line_id, + variation_category_list=[], + activate_kw=activate_kw) + else: + # from duplicated original line + cp = tryMethodCallWithTemporaryPermission( + delivery, 'Copy or Move', + lambda parent, *ids: + parent._duplicate(parent.manage_copyObjects(ids=ids))[0], + (delivery, old_delivery_line.getId()), {}, CopyError) + delivery_line = delivery[cp['new_id']] + # reset variation category list + delivery_line.setVariationCategoryList([]) + # delete non-split movements + keep_id_list = [y.getDeliveryValue().getId() for y in movement_list] + delete_id_list = [x.getId() for x in delivery_line.contentValues() \ + if x.getId() not in keep_id_list] + delivery_line.deleteContent(delete_id_list) + + return delivery_line + + def _createDeliveryCell(self, delivery_line, movement, activate_kw, + base_id, cell_key): + """ + Refer to the docstring in OrderBuilder. + Unlike OrderBuilder, DeliveryBuilder needs to respect + existing relationship. + """ + try: + old_cell = movement.getDeliveryValue() + except AttributeError: + old_cell = None + if old_cell is None: + # from scratch + cell = delivery_line.newCell(base_id=base_id, \ + portal_type=self.getDeliveryCellPortalType(), + activate_kw=activate_kw,*cell_key) + else: + # from duplicated original line + cp = tryMethodCallWithTemporaryPermission( + delivery_line, 'Copy or Move', + lambda parent, *ids: + parent._duplicate(parent.manage_copyObjects(ids=ids))[0], + (delivery_line, old_cell.getId()), {}, CopyError) + cell = delivery_line[cp['new_id']] + + return cell -- 2.30.9