diff --git a/product/ERP5Legacy/Document/BusinessPath.py b/product/ERP5Legacy/Document/BusinessPath.py index 11e523f11d41127440eabca14682a9615d7dbc28..35e46a6a4b31c2932bcf3661dde75cf0137e6018 100644 --- a/product/ERP5Legacy/Document/BusinessPath.py +++ b/product/ERP5Legacy/Document/BusinessPath.py @@ -1,4 +1,885 @@ -from Products.ERP5.Document.BusinessLink import BusinessLink +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# Yusuke Muraoka <yusuke@nexedi.com> +# 艁ukasz Nowak <luke@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 advised 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. +# +############################################################################## -BusinessPath = BusinessLink +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet, interfaces +from Products.ERP5.Document.Path import Path +from Products.ERP5.Document.Predicate import Predicate + +import zope.interface + +class BusinessPath(Path, Predicate): + """ + The BusinessPath class embeds all information related to + lead times and parties involved at a given phase of a business + process. + + BusinessPath are also used as helper to build deliveries from + buildable movements. + + The idea is to invoke isBuildable() on the collected simulation + movements (which are orphan) during build "after select" process + + Here is the typical code of an alarm in charge of the building process:: + + builder = portal_deliveries.a_delivery_builder + for business_path in builder.getDeliveryBuilderRelatedValueList(): + builder.build(causality_uid=business_path.getUid(),) # Select movements + + Pros: global select is possible by not providing a causality_uid + Cons: global select retrieves long lists of orphan movements which + are not yet buildable + the build process could be rather slow or require activities + + TODO: + - merge build implementation from erp5_bpm business template to ERP5 + product code with backward compatibility + """ + meta_type = 'ERP5 Business Path' + portal_type = 'Business Path' + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Declarative properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Folder + , PropertySheet.Comment + , PropertySheet.Arrow + , PropertySheet.Chain + , PropertySheet.SortIndex + , PropertySheet.BusinessPath + , PropertySheet.Reference + ) + + + # IArrowBase implementation + security.declareProtected(Permissions.AccessContentsInformation, + 'getSourceArrowBaseCategoryList') + def getSourceArrowBaseCategoryList(self): + """ + Returns all categories which are used to define the source + of this Arrow + """ + # Naive implementation - we must use category groups instead - XXX + return ('source', + 'source_account', + 'source_administration', + #'source_advice', + #'source_carrier', + #'source_decision', + 'source_function', + 'source_payment', + 'source_project', + #'source_referral', + 'source_section', + #'source_trade', + #'source_transport' + ) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getDestinationArrowBaseCategoryList') + def getDestinationArrowBaseCategoryList(self): + """ + Returns all categories which are used to define the destination + of this Arrow + """ + # Naive implementation - we must use category groups instead - XXX + return ('destination', + 'destination_account', + 'destination_administration', + #'destination_advice', + #'destination_carrier', + #'destination_decision', + 'destination_function', + 'destination_payment', + 'destination_project', + #'destination_referral', + 'destination_section', + #'destination_trade', + #'destination_transport' + ) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getArrowCategoryDict') + def getArrowCategoryDict(self, context=None, **kw): + result = {} + dynamic_category_list = self._getDynamicCategoryList(context) + for base_category in self.getSourceArrowBaseCategoryList() +\ + self.getDestinationArrowBaseCategoryList(): + category_url_list = Path._getAcquiredCategoryMembershipList( + self, base_category, **kw) + if len(category_url_list) == 0 and context is not None: + category_url_list = self._filterCategoryList(dynamic_category_list, + base_category, **kw) + if len(category_url_list) > 0: + result[base_category] = category_url_list + return result + + # ICategoryAccessProvider overridden methods + def _getCategoryMembershipList(self, category, **kw): + """ + Overridden in order to take into account dynamic arrow categories in case if no static + categories are set on Business Path + """ + context = kw.pop('context', None) + result = Path._getCategoryMembershipList(self, category, **kw) + if len(result) > 0: + return result + if context is not None: + dynamic_category_list = self._getDynamicCategoryList(context) + dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw) + result = dynamic_category_list + return result + + def _getAcquiredCategoryMembershipList(self, category, **kw): + """ + Overridden in order to take into account dynamic arrow categories in case if no static + categories are set on Business Path + """ + context = kw.pop('context', None) + result = Path._getAcquiredCategoryMembershipList(self, category, **kw) + if len(result) > 0: + return result + if context is not None: + dynamic_category_list = self._getDynamicCategoryList(context) + dynamic_category_list = self._filterCategoryList(dynamic_category_list, category, **kw) + result = dynamic_category_list + return result + + def _filterCategoryList(self, category_list, category, spec=(), + filter=None, portal_type=(), base=0, + keep_default=1, checked_permission=None): + """ + XXX - implementation missing + TBD - look at CategoryTool._buildFilter for inspiration + """ + # basic filtering: + # * remove categories which base name is not category + # * respect base parameter + prefix = category + '/' + start_index = not base and len(prefix) or 0 + return [category[start_index:] + for category in category_list + if category.startswith(prefix)] + + # Dynamic context based categories + def _getDynamicCategoryList(self, context): + return self._getDynamicSourceCategoryList(context) \ + + self._getDynamicDestinationCategoryList(context) + + def _getDynamicSourceCategoryList(self, context): + method_id = self.getSourceMethodId() + if method_id: + method = getattr(self, method_id) + return method(context) + return [] + + def _getDynamicDestinationCategoryList(self, context): + method_id = self.getDestinationMethodId() + if method_id: + method = getattr(self, method_id) + return method(context) + return [] + + # IBusinessBuildable implementation + def isBuildable(self, explanation): + """ + """ + if self.isCompleted(explanation) or self.isFrozen(explanation): + return False # No need to build what was already built or frozen + + # check if there is at least one simulation movement which is not + # delivered + for simulation_movement in self.getRelatedSimulationMovementValueList( + explanation): + if simulation_movement.getDeliveryValue() is None: + break + else: + # if all simulation movements are delivered, we can bail out + return False + + predecessor = self.getPredecessorValue() + if predecessor is None: + return True + # XXX FIXME TODO + # For now isPartiallyCompleted is used, as it was + # assumed to not implement isPartiallyBuildable, so in reality + # isBuildable is implemented like isPartiallyBuildable + # + # But in some cases it might be needed to implement + # isPartiallyBuildable, than isCompleted have to be used here + # + # Such cases are Business Processes using sequence not related + # to simulation tree with much of compensations + return predecessor.isPartiallyCompleted(explanation) + + def isPartiallyBuildable(self, explanation): + """ + Not sure if this will exist some day XXX + """ + + def _getExplanationUidList(self, explanation): + """Helper method to fetch really explanation related movements""" + explanation_uid_list = [explanation.getUid()] + # XXX: getCausalityRelatedValueList is oversimplification, assumes + # that documents are sequenced like simulation movements, which + # is wrong + for found_explanation in explanation.getCausalityRelatedValueList( + portal_type=self.getPortalDeliveryTypeList()) + \ + explanation.getCausalityValueList(): + explanation_uid_list.extend(self._getExplanationUidList( + found_explanation)) + return explanation_uid_list + + def build(self, explanation): + """ + Build + """ + builder_list = self.getDeliveryBuilderValueList() # Missing method + for builder in builder_list: + # chosen a way that builder is good enough to decide to select movements + # which shall be really build (movement selection for build is builder + # job, not business path job) + builder.build(select_method_dict={ + 'causality_uid': self.getUid(), + 'explanation_uid': self._getExplanationUidList(explanation) + }) + + # IBusinessCompletable implementation + security.declareProtected(Permissions.AccessContentsInformation, + 'isCompleted') + def isCompleted(self, explanation): + """ + Looks at all simulation related movements + and checks the simulation_state of the delivery + """ + acceptable_state_list = self.getCompletedStateList() + for movement in self.getRelatedSimulationMovementValueList(explanation): + if movement.getSimulationState() not in acceptable_state_list: + return False + return True + + security.declareProtected(Permissions.AccessContentsInformation, + 'isPartiallyCompleted') + def isPartiallyCompleted(self, explanation): + """ + Looks at all simulation related movements + and checks the simulation_state of the delivery + """ + acceptable_state_list = self.getCompletedStateList() + for movement in self.getRelatedSimulationMovementValueList(explanation): + if movement.getSimulationState() in acceptable_state_list: + return True + return False + + security.declareProtected(Permissions.AccessContentsInformation, + 'isFrozen') + def isFrozen(self, explanation): + """ + Looks at all simulation related movements + and checks if frozen + """ + movement_list = self.getRelatedSimulationMovementValueList(explanation) + if len(movement_list) == 0: + return False # Nothing to be considered as Frozen + for movement in movement_list: + if not movement.isFrozen(): + return False + return True + + def _recurseGetValueList(self, document, portal_type): + """Helper method to recurse documents as deep as possible and returns + list of document values matching portal_type""" + return_list = [] + for subdocument in document.contentValues(): + if subdocument.getPortalType() == portal_type: + return_list.append(subdocument) + return_list.extend(self._recurseGetValueList(subdocument, portal_type)) + return return_list + + def isMovementRelatedWithMovement(self, movement_value_a, movement_value_b): + """Checks if self is parent or children to movement_value + + This logic is Business Process specific for Simulation Movements, as + sequence of Business Process is not related appearance of Simulation Tree + + movement_value_a, movement_value_b - movements to check relation between + """ + movement_a_path = '%s/' % movement_value_a.getRelativeUrl() + movement_b_path = '%s/' % movement_value_b.getRelativeUrl() + + if movement_a_path == movement_b_path or \ + movement_a_path.startswith(movement_b_path) or \ + movement_b_path.startswith(movement_a_path): + return True + return False + + def _isDeliverySimulationMovementRelated(self, simulation_movement, + delivery_simulation_movement_list): + """Helper method, which checks if simulation_movement is BPM like related + with delivery""" + for delivery_simulation_movement in delivery_simulation_movement_list: + if self.isMovementRelatedWithMovement(delivery_simulation_movement, + simulation_movement): + return True + return False + + # IBusinessPath implementation + security.declareProtected(Permissions.AccessContentsInformation, + 'getRelatedSimulationMovementValueList') + def getRelatedSimulationMovementValueList(self, explanation): + """ + Returns recursively all Simulation Movements indirectly related to explanation and self + + As business sequence is not related to simulation tree need to built + full simulation trees per applied rule + """ + portal_catalog = self.getPortalObject().portal_catalog + + delivery_simulation_movement_list = portal_catalog( + delivery_uid=[x.getUid() for x in explanation.getMovementList()]) + + related_list = self.getBusinessPathClosure(delivery_simulation_movement_list) + + self_url = self.getRelativeUrl() + return [m for m in related_list if m.getCausality() == self_url] + + def getBusinessPathClosure(self, movement_list): + """ + Returns a list of Simulation Movement that are related to + movement_list. + "related" means that each of the returned Movement + will be an ancestor or a descendant of a movement in movement_list + + Formally, this method returns all Simulation Movements in: + ancestors(movement_list) U descendants(movement_list) + """ + # We need to find all ancestors of movement_list, as well as all + # of its descendants. + # When A is an ancestor of B we have: + # ancestors(B) > ancestors(A) and + # and + # descendants(A) > descendants(B) + # In this sense it only makes sense to compute descendants of A + # and ancestors of B. + # + # To do this we construct a tree of all (physical) paths leading + # to each movement in movement_list. This tree can be seen + # as a subtree of the whole Simulation Tree, or as a coloration + # of the Simulation Tree. + # Then, for each tree leaf, if that leaf has an non-root ancestor, + # we remove the leaf and only keep the ancestor: + # Because of the above properties, + # closure({leaf, ancestor}) == closure({ancestor}) + # which ensures that at the end of the coloration, + # closure(colored_tree) == closure(movement_list) + colored_tree_dict = dict() + + leaf_marker = object() + for simulation_movement in movement_list: + # remove portal_simulation from the path + component_list = simulation_movement.getRelativeUrl().split("/")[1:] + + cur = colored_tree_dict + for component in component_list[:-1]: + cur = cur.setdefault(component, {}) + if cur == leaf_marker: + # an ancestor of simulation_movement was colored before + break + else: + # note that we remove possibly-colored-before descendants + cur[component_list[-1]] = leaf_marker + + related_list = [] + def closure(root, path_item_tree): + """ + recursive helper filling related_list with: + nodes(tree) U descendants(leafs(tree)) + + root is a zodb object where the path_item_tree should be rooted. + """ + for k, v in path_item_tree.iteritems(): + cur = root[k] + # XXX maybe using parity Applied Rule / Simulation Movement is enough? + if cur.getPortalType() == 'Simulation Movement': + related_list.append(cur) + if v == leaf_marker: + related_list.extend(self._recurseGetValueList(cur, 'Simulation Movement')) + else: + closure(cur, v) + closure(self.getPortalObject().portal_simulation, colored_tree_dict) + return related_list + + def getExpectedQuantity(self, explanation, *args, **kwargs): + """ + Returns the expected stop date for this + path based on the explanation. + + XXX predecessor_quantity argument is required? + """ + if self.getQuantity(): + return self.getQuantity() + elif self.getEfficiency(): + return explanation.getQuantity() * self.getEfficiency() + else: + return explanation.getQuantity() + + def getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs): + """ + Returns the expected start date for this + path based on the explanation. + + predecessor_date -- if provided, computes the date base on the + date value provided + """ + return self._getExpectedDate(explanation, + self._getRootExplanationExpectedStartDate, + self._getPredecessorExpectedStartDate, + self._getSuccessorExpectedStartDate, + predecessor_date=predecessor_date, + *args, **kwargs) + + def _getRootExplanationExpectedStartDate(self, explanation, *args, **kwargs): + if self.getParentValue().isStartDateReferential(): + return explanation.getStartDate() + else: + expected_date = self.getExpectedStopDate(explanation, *args, **kwargs) + if expected_date is not None: + return expected_date - self.getLeadTime() + + def _getPredecessorExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs): + if predecessor_date is None: + node = self.getPredecessorValue() + if node is not None: + predecessor_date = node.getExpectedCompletionDate(explanation, *args, **kwargs) + if predecessor_date is not None: + return predecessor_date + self.getWaitTime() + + def _getSuccessorExpectedStartDate(self, explanation, *args, **kwargs): + node = self.getSuccessorValue() + if node is not None: + expected_date = node.getExpectedBeginningDate(explanation, *args, **kwargs) + if expected_date is not None: + return expected_date - self.getLeadTime() + + def getExpectedStopDate(self, explanation, predecessor_date=None, *args, **kwargs): + """ + Returns the expected stop date for this + path based on the explanation. + + predecessor_date -- if provided, computes the date base on the + date value provided + """ + return self._getExpectedDate(explanation, + self._getRootExplanationExpectedStopDate, + self._getPredecessorExpectedStopDate, + self._getSuccessorExpectedStopDate, + predecessor_date=predecessor_date, + *args, **kwargs) + + def _getRootExplanationExpectedStopDate(self, explanation, *args, **kwargs): + if self.getParentValue().isStopDateReferential(): + return explanation.getStopDate() + else: + expected_date = self.getExpectedStartDate(explanation, *args, **kwargs) + if expected_date is not None: + return expected_date + self.getLeadTime() + + def _getPredecessorExpectedStopDate(self, explanation, *args, **kwargs): + node = self.getPredecessorValue() + if node is not None: + expected_date = node.getExpectedCompletionDate(explanation, *args, **kwargs) + if expected_date is not None: + return expected_date + self.getWaitTime() + self.getLeadTime() + + def _getSuccessorExpectedStopDate(self, explanation, *args, **kwargs): + node = self.getSuccessorValue() + if node is not None: + return node.getExpectedBeginningDate(explanation, *args, **kwargs) + + def _getExpectedDate(self, explanation, root_explanation_method, + predecessor_method, successor_method, + visited=None, *args, **kwargs): + """ + Returns the expected stop date for this + path based on the explanation. + + root_explanation_method -- used when the path is root explanation + predecessor_method --- used to get expected date of side of predecessor + successor_method --- used to get expected date of side of successor + visited -- only used to prevent infinite recursion internally + """ + if visited is None: + visited = [] + + # mark the path as visited + if self not in visited: + visited.append(self) + + if self.isDeliverable(): + return root_explanation_method( + explanation, visited=visited, *args, **kwargs) + + predecessor_expected_date = predecessor_method( + explanation, visited=visited, *args, **kwargs) + + successor_expected_date = successor_method( + explanation, visited=visited, *args, **kwargs) + + if successor_expected_date is not None or \ + predecessor_expected_date is not None: + # return minimum expected date but it is not None + if successor_expected_date is None: + return predecessor_expected_date + elif predecessor_expected_date is None: + return successor_expected_date + else: + if predecessor_expected_date < successor_expected_date: + return predecessor_expected_date + else: + return successor_expected_date + + security.declareProtected(Permissions.AccessContentsInformation, + 'getBuildableMovementList') + def getBuildableMovementList(self, **sql_kw): + """ + Query catalog to find a list of movements related to this Business Path. + + Filter the results to return only Buildable Movements + + To specialize your builder, you can pass along SQL keywords + in sql_kw, for instance: + search_kw = {} + search_kw['movement.quantity'] = {'query':0, 'range':'neq'} + search_kw['movement.price'] = {'query':0, 'range':'neq'} + sql_kw = portal_catalog.buildSQLQuery(**search_kw) + """ + predecessor_state = self.getPredecessorValue() + if predecessor_state is None: + predecessor_uid_list = [] + else: + predecessor_list = predecessor_state.getSuccessorRelatedValueList() + predecessor_uid_list = [x.getUid() for x in predecessor_list] + all_movement_list = self.BusinessPath_zSelectBuildableMovementList( + business_path_uid=self.getUid(), + predecessor_uid_list=predecessor_uid_list, + **sql_kw) + + # select method should return only non-delivered movements, but + # maybe movements have been built in the meantime & catalog wasnt updated? + non_delivered_movement_list = filter(lambda x:x.getDeliveryValue() is None, + all_movement_list) + + return self.filterBuildableMovementList(non_delivered_movement_list) + + security.declareProtected(Permissions.AccessContentsInformation, + 'filterBuildableMovementList') + def filterBuildableMovementList(self, non_delivered_movement_list): + """ + Given a list of non delivered movements that all have "self" as + a causality value, return the ones that are buildables + + This is computed efficiently: movements are first separated into + distinct closures, and then filtering is made on each closure. + """ + predecessor_state = self.getPredecessorValue() + if predecessor_state is None: + # first Path in Process, all movements can be built + return non_delivered_movement_list + + predecessor_to_state_dict = {} + for pred in predecessor_state.getSuccessorRelatedValueList(): + predecessor_to_state_dict[pred] = frozenset(pred.getCompletedStateList()) + + root_dict = {} + # classify movements according to Root Applied Rules so we can look at + # them closure per closure + for movement in non_delivered_movement_list: + root_dict.setdefault(movement.getRootAppliedRule(), []).append(movement) + + result = [] + # for each root applied rule, get buildable Movements + for root_rule, movement_list in root_dict.iteritems(): + result.extend(self._filterBuildableInSameClosure(movement_list, + predecessor_to_state_dict)) + return result + + + + def _filterBuildableInSameClosure(self, movement_list, predecessor_to_state_dict): + """ + Return the buildable movements in movement_list. + + It is about finding in the tree the movements that have causalities in + predecessor_to_state_dict keys. + + Three main steps to find those movements, executed in least expensive + to most expensive order, hoping that step n allows us to return early + without having to execute n+1: + - look at ancestors of movement_list + - query catalog for descendants of movement_list, hoping that + it would be recent enough to list them all + - manually walk descendants of movement_list in ZODB + """ + buildable_list = [] + + # track relations within movement_list if any + # movement->(set of descendants in movement_list) + descendant_dict = {} + + # contains a movement -> (dict of predecessors that we still havent met) + # only contains the movements that have not proved to be unbuildable until + # now. + movement_looking_for_dict = {} + + def isBuiltAndCompleted(simulation, path): + return simulation.getDeliveryValue() is not None and \ + simulation.getSimulationState() in predecessor_to_state_dict[path] + + ### Step 1: + ## Explore ancestors + # + + for movement in movement_list: + # set of predecessors + looking_for = set(predecessor_to_state_dict) + current = movement.getParentValue() + + maybeBuildable = True + + # visit all parents until Root Applied Rule + while looking_for and maybeBuildable: + portal_type = current.getPortalType() + if portal_type == "Simulation Movement": + # exploring ancestors is a great way to initialize + # descendant_dict, while we're at it. + if current in movement_looking_for_dict: + descendant_dict.setdefault(current, set()).add(movement) + + path = current.getCausalityValue() + if path in looking_for: + looking_for.remove(path) + if not isBuiltAndCompleted(current, path): + maybeBuildable = False + + elif portal_type != "Applied Rule": + break + # XXX or maybe directly go up by two levels? + current = current.getParentValue() + + if maybeBuildable: + if not looking_for: + buildable_list.append(movement) + else: + movement_looking_for_dict[movement] = looking_for + + # Maybe we're lucky, and we've found all predecessors of all + # movements. + # We can thus return the buildable ones and we're done. + if not movement_looking_for_dict: + return buildable_list + + def updateDescendantDictAndReturnSmallestAncestorSet(): + """ + Remove from descendant_dict the movements that are not + buildable. + + Returns the smallest set of ancestors A that satisfies: + - A <= movement_looking_for_dict.keys() + - descendants(A) = descendants(movement_looking_for_dict.keys()) + (a.k.a. for any ai, aj in A, ai is not a descendant or an ancestor + of aj) + """ + movement_to_query = set(movement_looking_for_dict) + + if descendant_dict: + # remove movements that have been eliminated + for k, v in descendant_dict.items(): + if k not in movement_looking_for_dict: + del descendant_dict[k] + else: + v.intersection_update(movement_looking_for_dict) + + movement_to_query.difference_update(v) + return movement_to_query + + movement_to_query = updateDescendantDictAndReturnSmallestAncestorSet() + + ### Step 2: + ## Try catalog to find descendant movements, knowing + # that it can be incomplete + + class treeNode(dict): + """ + Used to cache accesses to ZODB objects. + The idea is to put in visited_movement_dict the objects we've already + loaded from ZODB to avoid loading them again. + + - self represents a single ZODB container c + - self.visited_movement_dict contains an id->(ZODB obj) cache for + subobjects of c + - self[id] contains the treeNode representing c[id] + """ + def __init__(self): + dict.__init__(self) + self.visited_movement_dict = dict() + + path_tree = treeNode() + + def updateTree(simulation_movement, path): + """ + Mark simulation_movement as visited in the Tree + + Returns the list of movements in movement_looking_for_dict that + are ancestors of simulation_movement + """ + traversed = [] + + tree_node = path_tree + movement_path = simulation_movement.getPhysicalPath() + simulation_movement_id = movement_path[-1] + # find container + for path_id in movement_path[:-1]: + # mark traversed movements that are in movement_looking_for_dict + mvmt, ignored = tree_node.visited_movement_dict.get(path_id, (None, None)) + if mvmt is not None and mvmt in movement_looking_for_dict: + traversed.append(mvmt) + + tree_node = tree_node.setdefault(path_id, treeNode()) + + # and mark the object as visited + tree_node.visited_movement_dict[simulation_movement_id] = (simulation_movement, path) + return traversed + + # initialization + for movement in movement_looking_for_dict: + updateTree(movement, None) + + portal_catalog = self.getPortalObject().portal_catalog + catalog_simulation_movement_list = portal_catalog( + portal_type='Simulation Movement', + causality_uid=[p.getUid() for p in predecessor_to_state_dict], + path=['%s/%%' % m.getPath() for m in movement_to_query]) + + unbuildable = set() + for movement in catalog_simulation_movement_list: + path = movement.getCausalityValue() + traversed = updateTree(movement, path) + if not isBuiltAndCompleted(movement, path): + unbuildable.update(traversed) + + if len(unbuildable) == len(movement_looking_for_dict): + # the sets are equals + return buildable_list + + for m in unbuildable: + del movement_looking_for_dict[m] + + ### Step 3: + ## We had no luck, we have to explore descendant movements in ZODB + # + + def findInTree(movement): + # descend in the tree to find self: + tree_node = path_tree + for path_id in movement.getPhysicalPath(): + tree_node = tree_node.get(path_id, treeNode()) + return tree_node + + def descendantGenerator(document, tree_node, path_set_to_check): + """ + generator yielding Simulation Movement descendants of document. + It does _not_ explore the whole subtree if iteration is stopped. + + It uses the tree we built previously to avoid loading again ZODB + objects that we already loaded during catalog querying + + path_set_to_check contains a set of Business Paths that we are + interested in. A branch is only explored if this set is not + empty; a movement is only yielded if its causality value is in this set + """ + object_id_list = document.objectIds() + for id in object_id_list: + if id not in tree_node.visited_movement_dict: + # we had not visited it in step #2 + subdocument = document._getOb(id) + if subdocument.getPortalType() == "Simulation Movement": + path = subdocument.getCausalityValue() + t = (subdocument, path) + tree_node.visited_movement_dict[id] = t + if path in path_set_to_check: + yield t + else: + # it must be an Applied Rule + subtree = tree_node.get(id, treeNode()) + for d in descendantGenerator(subdocument, + subtree, + path_set_to_check): + yield d + + for id, t in tree_node.visited_movement_dict.iteritems(): + subdocument, path = t + if path is None: + # happens for movement in movement_list + continue + to_check = path_set_to_check + # do we need to change/copy the set? + if path in to_check: + if len(to_check) == 1: + # no more paths to check in this branch + continue + to_check = to_check.copy() + to_check.remove(path) + subtree = tree_node.get(id, treeNode()) + for d in descendantGenerator(subdocument, subtree, to_check): + yield d + + + for movement in updateDescendantDictAndReturnSmallestAncestorSet(): + tree_node = findInTree(movement) + remaining_path_set = movement_looking_for_dict[movement] + # find descendants + for descendant, path in descendantGenerator(self, + tree_node, + remaining_path_set): + if not isBuiltAndCompleted(descendant, path): + break + else: + buildable_list.append(movement) + buildable_list.extend(descendant_dict.get(movement, [])) + + return buildable_list diff --git a/product/ERP5Legacy/PropertySheet/BusinessPath.py b/product/ERP5Legacy/PropertySheet/BusinessPath.py new file mode 100644 index 0000000000000000000000000000000000000000..6df0058ed39a9aecbf113aabe476fe25be594d76 --- /dev/null +++ b/product/ERP5Legacy/PropertySheet/BusinessPath.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. +# 艁ukasz Nowak <luke@nexedi.com> +# Yusuke Muraoka <yusuke@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability 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 +# garantees 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. +# +############################################################################## + +class BusinessPath: + """ + Business Path properties + """ + _properties = ( + { 'id' : 'deliverable', # XXX: same is in Simulation property sheet + 'description' : 'If 1 it is related to root of simulation tree (root explanation)', + 'type' : 'boolean', + 'mode' : 'w' }, + { 'id' : 'source_method_id', + 'description' : 'ID of method to get source list of categories', + 'type' : 'string', + 'mode' : 'w' }, + { 'id' : 'destination_method_id', + 'description' : 'ID of method to get destination list of categories', + 'type' : 'string', + 'mode' : 'w' }, + { 'id' : 'lead_time', + 'description' : 'How much time shall be spent on path', + 'default' : 0.0, + 'type' : 'float', + 'mode' : 'w' }, + { 'id' : 'wait_time', + 'description' : 'How much time to wait before initiating path', + 'default' : 0.0, + 'type' : 'float', + 'mode' : 'w' }, + { 'id' : 'completed_state', + 'description' : 'List of states for which related Simulation ' + 'Movement is considered as completed', + 'type' : 'lines', + 'default' : [], + 'multivalued' : 1, + 'mode' : 'w' }, + { 'id' : 'frozen_state', + 'description' : 'List of states for which related Simulation ' + 'Movement is considered as frozen', + 'type' : 'lines', + 'default' : [], + 'multivalued' : 1, + 'mode' : 'w' }, + ) + _categories = ( 'delivery_builder', 'order_builder', 'end_of', + 'trade_phase' , 'incoterm') diff --git a/product/ERP5Legacy/tests/testLegacyBPMCore.py b/product/ERP5Legacy/tests/testLegacyBPMCore.py index 371d3673cfdbad6edb9e32ff6cc30e136a930382..be27d823a18068fab260c2cbad32f9f6e0ae1ee7 100644 --- a/product/ERP5Legacy/tests/testLegacyBPMCore.py +++ b/product/ERP5Legacy/tests/testLegacyBPMCore.py @@ -42,7 +42,8 @@ class TestBPMMixin(ERP5TypeTestCase): def getBusinessTemplateList(self): return ('erp5_base', 'erp5_pdm', 'erp5_trade', 'erp5_accounting', - 'erp5_invoicing', 'erp5_simplified_invoicing', 'erp5_legacy') + 'erp5_invoicing', 'erp5_simplified_invoicing', 'erp5_simulation', + 'erp5_legacy') business_process_portal_type = 'Business Process' business_path_portal_type = 'Business Path'