From 9880c1197a1cd80e00a7ef68848df79d5b66bc88 Mon Sep 17 00:00:00 2001 From: Jean-Paul Smets <jp@nexedi.com> Date: Mon, 24 May 2010 20:47:27 +0000 Subject: [PATCH] Some more ideas where BPM is heading to git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@35585 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/BusinessPath.py | 63 +- product/ERP5/Document/BusinessProcess.py | 684 +++++++++++++++----- product/ERP5/ExplanationCache.py | 51 +- product/ERP5/interfaces/business_path.py | 13 + product/ERP5/interfaces/business_process.py | 2 +- 5 files changed, 611 insertions(+), 202 deletions(-) diff --git a/product/ERP5/Document/BusinessPath.py b/product/ERP5/Document/BusinessPath.py index 39052eeba4..155c839846 100644 --- a/product/ERP5/Document/BusinessPath.py +++ b/product/ERP5/Document/BusinessPath.py @@ -34,42 +34,12 @@ 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 +from Products.ERP5.ExplanationCache import _getExplanationCache import zope.interface from zLOG import LOG -class ExplanationCache: - """ExplanationCache provides a central access to - all parameters and values which are needed to process - an explanation. It is based on the idea that a value is calculated - once and once only, as a way to accelerate performance of algorithms - related to an explanation. - - 'explanation_uid': self._getExplanationUidList(explanation) # XXX-JPS why do we need explanation_uid ? and why a list - 'simulation_path': simulation_path, - - explanation_uid = self._getExplanationUidList(explanation) # A hint value to reduce the size of the tree - simulation_path = '/erp5/p.../%' # A list of path - -""" - def __init__(self, explanation): - """ - """ - self.explanation = explanation - - def getRootExplanationUidList(self): - """ - """ - - def getSimulationPathPatternList(self): - """ - """ - -def _getExplanationCache(explanation): - # XXX-JPS Cache this in a transaction variable or better - return ExplanationCache(explanation) - class BusinessPath(Path, Predicate): """ The BusinessPath class embeds all information related to @@ -175,6 +145,12 @@ class BusinessPath(Path, Predicate): #'destination_transport' ) + # Helper Methods + def _getExplanationRelatedSimulationMovementValueList(self, explanation): + explanation_cache = _getExplanationCache(explanation) + return explanation_cache.getBusinessPathRelatedSimulationMovementValueList(self) + + # XXX-JPS UNkonwn ? security.declareProtected(Permissions.AccessContentsInformation, 'getArrowCategoryDict') def getArrowCategoryDict(self, context=None, **kw): # XXX-JPS do we need it in API ? @@ -271,6 +247,22 @@ class BusinessPath(Path, Predicate): method_id = self.getCompletionDateMethodId() method = getattr(movement, method_id) # We wish to raise if it does not exist return method() + + def getCompletionDate(self, explanation): + """Returns the date of completion of the movemnet + based on paremeters of the business path. This complete date can be + the start date, the stop date, the date of a given workflow transition + on the explaining delivery, etc. + + XXX - DOC + + movement -- a Simulation Movement + """ + date_list = [] + for movement in self._getExplanationRelatedSimulationMovementValueList(explanation): + date_list.append(self.getMovementCompletionDate(movement)) + + return max(date_list) security.declareProtected(Permissions.AccessContentsInformation, 'getExpectedQuantity') @@ -363,6 +355,15 @@ class BusinessPath(Path, Predicate): return False return True + def isDelivered(self, explanation): + """XXX + """ + for simulation_movement in self._getExplanationRelatedSimulationMovementValueList( + explanation): + if not simulation_movement.getDelivery(): + return False + return True + def build(self, explanation): """Builds all related movements in the simulation using the builders defined on the Business Path. diff --git a/product/ERP5/Document/BusinessProcess.py b/product/ERP5/Document/BusinessProcess.py index 64535eab21..a2c4ed801f 100644 --- a/product/ERP5/Document/BusinessProcess.py +++ b/product/ERP5/Document/BusinessProcess.py @@ -32,6 +32,7 @@ from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions, PropertySheet, interfaces from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5.Document.Path import Path +from Products.ERP5.ExplanationCache import _getExplanationCache, _getBusinessPathClosure import zope.interface @@ -43,6 +44,8 @@ class BusinessProcess(Path, XMLObject): TODO: - finish interface implementation + RENAME: + getPathValueList -> getBusinessPathValueList """ meta_type = 'ERP5 Business Process' portal_type = 'Business Process' @@ -66,19 +69,23 @@ class BusinessProcess(Path, XMLObject): zope.interface.implements(interfaces.IBusinessProcess, interfaces.IArrowBase) - # Access to path and states of the business process - security.declareProtected(Permissions.AccessContentsInformation, 'getPathValueList') - def getPathValueList(self, trade_phase=None, context=None, **kw): - """ - Returns all Path of the current BusinessProcess which - are matching the given trade_phase and the optional context. + # IBusinessPathProcess implementation + security.declareProtected(Permissions.AccessContentsInformation, 'getBusinessPathValueList') + def getBusinessPathValueList(self, trade_phase=None, context=None, + predecessor=None, successor=None, **kw): + """Returns all Path of the current BusinessProcess which + are matching the given trade_phase and the optional context. + + trade_phase -- filter by trade phase + + context -- a context to test each Business Path on + and filter out Business Path which do not match - trade_phase -- a single trade phase category or a list of - trade phases + predecessor -- filter by trade state predecessor - context -- the context to search matching predicates for + successor -- filter by trade state successor - **kw -- same parameters as for searchValues / contentValues + **kw -- same arguments as those passed to searchValues / contentValues """ if trade_phase is None: trade_phase = set() @@ -87,14 +94,22 @@ class BusinessProcess(Path, XMLObject): else: trade_phase = set(trade_phase) result = [] + if kw.get('portal_type', None) is None: + kw['portal_type'] = self.getPortalBusinessPathTypeList() + if kw.get('sort_on', None) is None: + kw['sort_on'] = 'int_index' + original_business_path_list = self.objectValues(**kw), # Why Object Values ??? XXX-JPS if len(trade_phase) == 0: - return result - # Separate the selection of business paths into twp steps + return original_business_path_list # If not trade_phase is specified, return all Business Path + # Separate the selection of business paths into two steps # for easier debugging. # First, collect business paths which can be applicable to a given context. business_path_list = [] - for business_path in self.objectValues(portal_type='Business Path', - sort_on='int_index'): + for business_path in original_business_path_list: + if predecessor is not None and business_path.getPredecessor() != predecessor: + break # Filter our business path which predecessor does not match + if successor is not None and business_path.getSuccessor() != successor: + break # Filter our business path which predecessor does not match if trade_phase.intersection(business_path.getTradePhaseList()): business_path_list.append(business_path) # Then, filter business paths by Predicate API. @@ -106,174 +121,430 @@ class BusinessProcess(Path, XMLObject): result.append(business_path) return result - security.declareProtected(Permissions.AccessContentsInformation, 'getStateValueList') - def getStateValueList(self, *args, **kw): - """ - Returns all states of the business process. The method - **kw parameters follows the API of searchValues / contentValues - """ - # Naive implementation to redo XXX - kw['portal_type'] = "Business Path" - return [x for x in [y.getSuccessorValue() for y in \ - self.contentValues(*args, **kw)] if x is not None] + def isBusinessPathCompleted(self, explanation, business_path): + """Returns True if given Business Path document + is completed in the context of provided explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + business_path -- a Business Path document + """ + # Return False if Business Path is not completed + if not business_path.isCompleted(explanation): + return False + predecessor_state = business_path.getPredecessor() + if not predecessor_state: + # This is a root business path, no predecessor + # so no need to do any recursion + return True + if self.isTradeStateCompleted(explanation, predecessor_state): + # If predecessor state is globally completed for the + # given explanation, return True + # Please note that this is a specific case for a Business Process + # built using asUnionBusinessProcess. In such business process + # a business path may be completed even if its predecessor state + # is not + return True + # Build the closure business process which only includes those business + # path wich are directly related to the current business path but DO NOT + # narrow down the explanation else we might narrow down so much that + # it becomes an empty set + closure_process = _getBusinessPathClosure(explanation, business_path) + return closure_process.isTradeStateCompleted(explanation, predecessor_state) + + def isBusinessPathPartiallyCompleted(self, explanation, business_path): + """Returns True if given Business Path document + is partially completed in the context of provided explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + business_path -- a Business Path document + """ + # Return False if Business Path is not partially completed + if not business_path.isPartiallyCompleted(explanation): + return False + predecessor_state = business_path.getPredecessor() + if not predecessor_state: + # This is a root business path, no predecessor + # so no need to do any recursion + return True + if self.isTradeStatePartiallyCompleted(explanation, predecessor_state): + # If predecessor state is globally partially completed for the + # given explanation, return True + # Please note that this is a specific case for a Business Process + # built using asUnionBusinessProcess. In such business process + # a business path may be partially completed even if its predecessor + # state is not + return True + # Build the closure business process which only includes those business + # path wich are directly related to the current business path but DO NOT + # narrow down the explanation else we might narrow down so much that + # it becomes an empty set + closure_process = _getBusinessPathClosure(explanation, business_path) + return closure_process.isTradeStatePartiallyCompleted(explanation, + predecessor_state) + + def getExpectedBusinessPathCompletionDate(explanation, business_path, + delay_mode=None): + """Returns the expected completion date of given Business Path document + in the context of provided explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + business_path -- a Business Path document + + delay_mode -- optional value to specify calculation mode ('min', 'max') + if no value specified use average delay + """ + closure_process = _getBusinessPathClosure(explanation, business_path) + # XXX use explanatoin cache to optimize + predecessor = business_path.getPredecessor() + if closure_process.isTradeStateRootState(predecessor): + return business_path.getCompletionDate(explanation) + + reference_date = closure_process.getExpectedTradeStateCompletionDate(explanation, predecessor) + + return reference_date + wait_time + lead_time + - # Access to path and states of the business process - def isCompleted(self, explanation): - """ - True if all states are completed - """ - for state in self.getStateValueList(): - if not state.isCompleted(explanation): - return False - return True - - def isBuildable(self, explanation): - """ - True if all any path is buildable - """ - return len(self.getBuildablePathValueList(explanation)) != 0 + def getExpectedBusinessPathStartAndStopDate(explanation, business_path, + delay_mode=None): + """Returns the expected start and stop dates of given Business Path + document in the context of provided explanation. - def getBuildablePathValueList(self, explanation): - """ - Returns the list of Business Path which are ready to - be built - """ - return filter(lambda x:x.isBuildable(explanation), - self.objectValues(portal_type='Business Path')) + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree - def getCompletedStateValueList(self, explanation): - """ - Returns the list of Business States which are finished - """ - return filter(lambda x:x.isCompleted(explanation), self.getStateValueList()) + business_path -- a Business Path document - def getPartiallyCompletedStateValueList(self, explanation): + delay_mode -- optional value to specify calculation mode ('min', 'max') + if no value specified use average delay """ - Returns the list of Business States which are finished + closure_process = _getBusinessPathClosure(explanation, business_path) + # XXX use explanatoin cache to optimize + trade_date = self.getTradeDate() + if trade_date is not None: + reference_date = closure_process.gettExpectedTradePhaseCompletionDate(explanation, trade_date) + else: + predecessor = business_path.getPredecessor() # XXX-JPS they should all have + reference_date = closure_process.getExpectedTradeStateCompletionDate(explanation, predecessor) + + start_date = reference_date + wait_time + stop_date = start_date + lead_time # XXX-JPS use appropriate values + + return start_date, stop_date + + # IBuildableBusinessPathProcess implementation + def getBuildableBusinessPathValueList(self, explanation): + """Returns the list of Business Path which are buildable + by taking into account trade state dependencies between + Business Path. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - return filter(lambda x:x.isPartiallyCompleted(explanation), self.getStateValueList()) + result = [] + for business_path in self.getBusinessPathValueList(): + if self.isBusinessPathBuildable(explanation, business_path): + result.append(business_path) + return result + + def getPartiallyBuildableBusinessPathValueList(self, explanation): + """Returns the list of Business Path which are partially buildable + by taking into account trade state dependencies between + Business Path. - def getLatestCompletedStateValue(self, explanation): + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - Returns the most advanced completed state + result = [] + for business_path in self.getBusinessPathValueList(): + if self.isBusinessPathPartiallyBuildable(explanation, business_path): + result.append(business_path) + return result + + def isBusinessPathBuildable(self, explanation, business_path): + """Returns True if any of the related Simulation Movement + is buildable and if the predecessor trade state is completed. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + business_path -- a Business Path document + """ + # If everything is delivered, no need to build + if business_path.isDelivered(explanation): + return False + # We must take the closure cause only way to combine business process + closure_process = _getBusinessPathClosure(explanation, business_path) + predecessor = business_path.getPredecessor() + return closure_process.isTradeStateCompleted(predecessor) + + def isBusinessPathPartiallyBuildable(self, explanation, business_path): + """Returns True if any of the related Simulation Movement + is buildable and if the predecessor trade state is partially completed. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + business_path -- a Business Path document + """ + # If everything is delivered, no need to build + if business_path.isDelivered(explanation): + return False + # We must take the closure cause only way to combine business process + closure_process = _getBusinessPathClosure(explanation, business_path) + predecessor = business_path.getPredecessor() + return closure_process.isTradeStatePartiallyCompleted(predecessor) + + # ITradeStateProcess implementation + def getTradeStateList(): + """Returns list of all trade_state of this Business Process + by looking at successor and predecessor values of contained + Business Path. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + """ + result = set() + for business_path in self.getBusinessPathValueList(): + result.add(business_path.getPredecessor()) + result.add(business_path.getSuccessor()) + return result + + def getSuccessorTradeStateList(explanation, trade_state): + """Returns the list of successor states in the + context of given explanation. This list is built by looking + at all successor of business path involved in given explanation + and which predecessor is the given trade_phase. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_state -- a Trade State category """ - for state in self.getCompletedStateValueList(explanation): - for path in state.getPredecessorRelatedValueList(): - if not path.isCompleted(explanation): - return state - return None + result = set() + for business_path in self.getBusinessPathValueList(): + if business_path.getPredecessor() == trade_state: + result.add(business_path.getSuccessor()) + return result - def getLatestPartiallyCompletedStateValue(self, explanation): + def getPredecessorTradeStateList(explanation, trade_state): + """Returns the list of predecessor states in the + context of given explanation. This list is built by looking + at all predecessor of business path involved in given explanation + and which sucessor is the given trade_phase. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_state -- a Trade State category """ - Returns the most advanced completed state + result = set() + for business_path in self.getBusinessPathValueList(): + if business_path.getSuccessor() == trade_state: + result.add(business_path.getPredecessor()) + return result + + def getCompletedTradeStateList(explanation): + """Returns the list of Trade States which are completed + in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - for state in self.getCompletedStateValueList(explanation): - for path in state.getPredecessorRelatedValueList(): - if not path.isPartiallyCompleted(explanation): - return state - return None + return filter(lambda x:self.isTradeStateCompleted(explanation, x), self.getTradeStateList()) + + def getPartiallyCompletedTradeStateList(explanation): + """Returns the list of Trade States which are partially + completed in the context of given explanation. - def getLatestCompletedStateValueList(self, explanation): + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - Returns the most advanced completed state + return filter(lambda x:self.isTradeStatePartiallyCompleted(explanation, x), self.getTradeStateList()) + + def getLatestCompletedTradeStateList(explanation): + """Returns the list of completed trade states which predecessor + states are completed and for which no successor state + is completed in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - result = [] - for state in self.getCompletedStateValueList(explanation): - for path in state.getPredecessorRelatedValueList(): - if not path.isCompleted(explanation): - result.append(state) + result = set() + for state in self.getCompletedTradeStateValue(explanation): + for business_path in state.getPredecessorRelatedValueList(): + if not self.isBusinessPathCompleted(explanation, business_path): + result.add(state) return result - def getLatestPartiallyCompletedStateValueList(self, explanation): - """ - Returns the most advanced completed state + def getLatestPartiallyCompletedTradeStateList(explanation): + """Returns the list of completed trade states which predecessor + states are completed and for which no successor state + is partially completed in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - result = [] - for state in self.getCompletedStateValueList(explanation): - for path in state.getPredecessorRelatedValueList(): - if not path.isPartiallyCompleted(explanation): - result.append(state) + result = set() + for state in self.getCompletedTradeStateValue(explanation): + for business_path in state.getPredecessorRelatedValueList(): + if not self.isBusinessPathPartiallyCompleted(explanation, business_path): + result.add(state) return result - def build(self, explanation_relative_url): - """ - Build whatever is buildable + def isTradeStateCompleted(explanation, trade_state): + """Returns True if all predecessor trade states are + completed and if no successor trade state is completed + in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_state -- a Trade State category """ - explanation = self.restrictedTraverse(explanation_relative_url) - for path in self.getBuildablePathValueList(explanation): - path.build(explanation) + for business_path in self.getBusinessPathValueList(successor=trade_state): + if not closure_process.isBusinessPathCompleted(explanation, business_path): + return False + return True - def isStartDateReferential(self): # XXX - not in interface - return self.getReferentialDate() == 'start_date' + def isTradeStatePartiallyCompleted(explanation, trade_state): + """Returns True if all predecessor trade states are + completed and if no successor trade state is partially completed + in the context of given explanation. - def isStopDateReferential(self): # XXX - not in interface - return self.getReferentialDate() == 'stop_date' + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree - def getTradePhaseList(self): + trade_state -- a Trade State category """ - Returns all trade_phase of this business process + for business_path in self.getBusinessPathValueList(successor=trade_state): + if not self.isBusinessPathPartiallyCompleted(explanation, business_path): + return False + return True + + def getExpectedTradeStateCompletionDate(explanation, trade_state, + delay_mode=None): + """Returns the date at which the give trade state is expected + to be completed in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_state -- a Trade State category + + delay_mode -- optional value to specify calculation mode ('min', 'max') + if no value specified use average delay """ - path_list = self.objectValues(portal_type=self.getPortalBusinessPathTypeList()) - return filter(None, [path.getTradePhase() - for path in path_list]) + date_list = [] + for business_path in self.getBusinessPathValueList(successor=trade_state): + date_list.append(self.getExpectedBusinessPathCompletionDate(explanation, business_path)) + return max(date_list) # XXX-JPS provide -infinite for... - def getRootExplanationPathValue(self): + # ITradePhaseProcess implementation + def getTradePhaseList(): + """Returns list of all trade_phase of this Business Process + by looking at trade_phase values of contained Business Path. """ - Returns a root path of this business process + result = set() + for business_path in self.getBusinessPathValueList(): + result.union(business_path.getTradePhaseList()) + return result + + def getCompletedTradePhaseList(explanation): + """Returns the list of Trade Phases which are completed + in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - path_list = self.objectValues(portal_type=self.getPortalBusinessPathTypeList()) - path_list = filter(lambda x: x.isDeliverable(), path_list) + return filter(lambda x:self.isTradePhaseCompleted(explanation, x), self.getTradePhaseList()) - if len(path_list) > 1: - raise Exception, "this business process has multi root paths" - - if len(path_list) == 1: - return path_list[0] + def getPartiallyCompletedTradePhaseList(explanation): + """Returns the list of Trade Phases which are partially completed + in the context of given explanation. - def getHeadPathValueList(self, trade_phase_list=None): + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - Returns a list of head path(s) of this business process + return filter(lambda x:self.isTradePhasePartiallyCompleted(explanation, x), self.getTradePhaseList()) - trade_phase_list -- used to filtering, means that discovering - a list of head path with the trade_phase_list + def isTradePhaseCompleted(explanation, trade_phase): + """Returns True all business path with given trade_phase + applicable to given explanation are completed. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_phase -- a Trade Phase category """ - head_path_list = list() - for state in self.getStateValueList(): - if len(state.getSuccessorRelatedValueList()) == 0: - head_path_list += state.getPredecessorRelatedValueList() + for business_path in self.getBusinessPathValueList(trade_phase=trade_phase): + if not self.isBusinessPathCompleted(explanation, business_path): + return False + return True - if trade_phase_list is not None: - _set = set(trade_phase_list) - _list = list() - # start to discover a head path with the trade_phase_list from head path(s) of whole - for path in head_path_list: - _list += self._getHeadPathValueList(path, _set) - head_path_list = map(lambda t: t[0], filter(lambda t: t != (None, None), _list)) + def isTradePhasePartiallyCompleted(explanation, trade_phase): + """Returns True at least one business path with given trade_phase + applicable to given explanation is partially completed + or completed. - return head_path_list + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree - def _getHeadPathValueList(self, path, trade_phase_set): - # if the path has target trade_phase, it is a head path. - _set = set(path.getTradePhaseList()) - if _set & trade_phase_set: - return [(path, _set & trade_phase_set)] + trade_phase -- a Trade Phase category + """ + for business_path in self.getBusinessPathValueList(trade_phase=trade_phase): + if not self.isBusinessPathPartiallyCompleted(explanation, business_path): + return False + return True - node = path.getSuccessorValue() - if node is None: - return [(None, None)] + def getExpectedTradePhaseCompletionDate(explanation, trade_phase, + delay_mode=None): + """Returns the date at which the give trade phase is expected + to be completed in the context of given explanation, taking + into account the graph of date constraints defined by business path + and business states. - _list = list() - for next_path in node.getPredecessorRelatedValueList(): - _list += self._getHeadPathValueList(next_path, trade_phase_set) - return _list + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + + trade_phase -- a Trade Phase category - def getRemainingTradePhaseList(self, explanation, trade_state, trade_phase_list=None): + delay_mode -- optional value to specify calculation mode ('min', 'max') + if no value specified use average delay """ - Returns the list of remaining trade phase for this - state based on the explanation. + date_list = [] + for business_path in self.getBusinessPathValueList(trade_phase=trade_phase): + date_list.append(self.getExpectedTradePhaseCompletionDate(explanation, business_path, delay_mode=delay_mode)) + return max(date_list) - trade_phase_list -- if provide, the result is filtered by it after collected + def getRemainingTradePhaseList(business_path, trade_phase_list=None): + """Returns the list of remaining trade phases which to be achieved + as part of a business process. This list is calculated by analysing + the graph of business path and trade states, starting from a given + business path. The result if filtered by a list of trade phases. This + method is useful mostly for production and MRP to manage a distributed + supply and production chain. + + business_path -- a Business Path document + + trade_phase_list -- if provided, the result is filtered by it after + being collected - ???? useful ? XXX-JPS ? + + NOTE: explanation is not involved here because we consider here that + self is the result of asUnionBusinessProcess and thus only contains + applicable Business Path to a given simulation subtree. Since the list + of remaining trade phases does not depend on exact values in the + simulation, we did not include the explanation. However, this makes the + API less uniform. """ + + # XXXX REVIEWJPS + remaining_trade_phase_list = [] for path in [x for x in self.objectValues(portal_type="Business Path") \ if x.getPredecessorValue() == trade_state]: @@ -297,38 +568,54 @@ class BusinessProcess(Path, XMLObject): return remaining_trade_phase_list - def isStatePartiallyCompleted(self, explanation, trade_state): - """ - If all path which reach this state are partially completed - then this state is completed + # IBusinessProcess global API + def isCompleted(explanation): + """Returns True is all applicable Trade States and Trade Phases + are completed in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - for path in [x for x in self.objectValues(portal_type="Business Path") \ - if x.getSuccessorValue() == trade_state]: - if not path.isPartiallyCompleted(explanation): + for state in self.getTradeStateList(): + if not state.isTradeStateCompleted(explanation): return False return True - def getExpectedStateCompletionDate(self, explanation, trade_state, *args, **kwargs): + def getExpectedCompletionDate(explanation, delay_mode=None): + """Returns the expected date at which all applicable Trade States and + Trade Phases are completed in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - Returns the expected completion date for this - state based on the explanation. + + def isBuildable(explanation): + """Returns True is one Business Path of this Business Process + is buildable in the context of given explanation. - explanation -- the document + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree """ - # Should be re-calculated? - # XXX : what is the purpose of the two following lines ? comment it until there is - # good answer - if 'predecessor_date' in kwargs: - del kwargs['predecessor_date'] - successor_list = [x for x in self.objectValues(portal_type="Business Path") \ - if x.getSuccessorValue() == trade_state] - date_list = self._getExpectedDateList(explanation, - successor_list, - self._getExpectedCompletionDate, - *args, - **kwargs) - if len(date_list) > 0: - return min(date_list) + return len(self.getBuildableBusinessPathValueList(explanation)) != 0 + + def isPartiallyBuildable(explanation): + """Returns True is one Business Path of this Business Process + is partially buildable in the context of given explanation. + + explanation -- an Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree + """ + return len(self.getPartiallyBuildableBusinessPathValueList(explanation)) != 0 + + def build(self, explanation): + """ + Build whatever is buildable + """ + for business_path in self.getBuildableBusinessPathValueList(explanation): + business_path.build(explanation) + + + # GARBAGE - XXXXXXXXXXXXXXXXXXXXXXX def getExpectedStateBeginningDate(self, explanation, trade_state, *args, **kwargs): """ @@ -381,6 +668,75 @@ class BusinessProcess(Path, XMLObject): return expected_date_list + def getRootExplanationPathValue(self): # XXX-JPS not in API + """ + Returns a root path of this business process + """ + path_list = self.objectValues(portal_type=self.getPortalBusinessPathTypeList()) + path_list = filter(lambda x: x.isDeliverable(), path_list) + + if len(path_list) > 1: + raise Exception, "this business process has multi root paths" + + if len(path_list) == 1: + return path_list[0] + + def getHeadPathValueList(self, trade_phase_list=None): # XXX-JPS not in API + """ + Returns a list of head path(s) of this business process + + trade_phase_list -- used to filtering, means that discovering + a list of head path with the trade_phase_list + """ + head_path_list = list() + for state in self.getStateValueList(): + if len(state.getSuccessorRelatedValueList()) == 0: + head_path_list += state.getPredecessorRelatedValueList() + + if trade_phase_list is not None: + _set = set(trade_phase_list) + _list = list() + # start to discover a head path with the trade_phase_list from head path(s) of whole + for path in head_path_list: + _list += self._getHeadPathValueList(path, _set) + head_path_list = map(lambda t: t[0], filter(lambda t: t != (None, None), _list)) + + return head_path_list + + def _getHeadPathValueList(self, path, trade_phase_set): + # if the path has target trade_phase, it is a head path. + _set = set(path.getTradePhaseList()) + if _set & trade_phase_set: + return [(path, _set & trade_phase_set)] + + node = path.getSuccessorValue() + if node is None: + return [(None, None)] + + _list = list() + for next_path in node.getPredecessorRelatedValueList(): + _list += self._getHeadPathValueList(next_path, trade_phase_set) + return _list + def _getExpectedCompletionDate(self, path, *args, **kwargs): return path.getExpectedStopDate(*args, **kwargs) + + # From Business Path + def _getExplanationUidList(self, explanation): + """ + Helper method to fetch really explanation related movements + """ # XXX-JPS this seems to be doing too much - why do you need many "explanations" + explanation_uid_list = [explanation.getUid()] + # XXX: getCausalityRelatedValueList is oversimplification, assumes + # that documents are sequenced like simulation movements, which + # is wrong + if getattr(explanation, "getCausalityValueList", None) is None: + return explanation_uid_list + for found_explanation in explanation.getCausalityRelatedValueList( # XXX-JPS this also seems exagerated, and breaking the APIs + portal_type=self.getPortalDeliveryTypeList()) + \ + explanation.getCausalityValueList(): # Wrong if an item + explanation_uid_list.extend(self._getExplanationUidList( + found_explanation)) + return explanation_uid_list + diff --git a/product/ERP5/ExplanationCache.py b/product/ERP5/ExplanationCache.py index e159274ea5..3aed3a9d32 100644 --- a/product/ERP5/ExplanationCache.py +++ b/product/ERP5/ExplanationCache.py @@ -51,12 +51,12 @@ class ExplanationCache: """ # Define share properties self.explanation = explanation - self.portal_catalog = getToolByName(explanation 'portal_catalog') + self.portal_catalog = getToolByName(explanation, 'portal_catalog') self.simulation_movement_cache = {} # Simulation Movement Cache self.explanation_uid_cache = [] self.explanation_path_pattern_cache = [] - def getDeliveryMovementList(self): + def _getDeliveryMovementList(self): """Returns self is explanation is a delivery line of the list of explanation delivery lines if explanation is a delivery @@ -80,7 +80,7 @@ class ExplanationCache: return self.explanation_uid_cache result = set() # For each delivery movement - for movement in self.getDeliveryMovementList(): + for movement in self._getDeliveryMovementList(): # For each simulation movement for simulation_movement in movement.getDeliveryRelatedValueList(): result.add(simulation_movement.getExplanationUid()) # XXX-JPS use new API later @@ -117,7 +117,7 @@ class ExplanationCache: local_path_dict[simulation_movement_id] = simulation_movement # For each delivery movement - for movement in self.getDeliveryMovementList(): + for movement in self._getDeliveryMovementList(): # For each simulation movement for simulation_movement in movement.getDeliveryRelatedValueList(): updatePathDict(simulation_movement) @@ -133,7 +133,7 @@ class ExplanationCache: else: browsePathDict('%s/%s' % (prefix, key), value) # Recursing with string append is slow XXX-JPS - browsePathDict('', path_dict) + browsePathDict('/', path_dict) self.explanation_path_pattern_cache = result return result @@ -143,7 +143,7 @@ class ExplanationCache: """ return self.getSimulationMovementList(causality_uid=business_path.getUid()) - def getSimulationMovementList(self, **kw) + def getSimulationMovementList(self, **kw): """Search Simulation Movements related to our explanation. Cache result so that the second time we saarch for the same list we need not involve the catalog again. @@ -157,9 +157,48 @@ class ExplanationCache: **kw) return self.simulation_movement_cache[kw_tuple] + def geBusinessPathClosure(business_path): + """Creates a Business Process by filtering out all Business Path + in self which are not related to a simulation movement + which is either or parent or a child of explanation simulations movements + caused by 'business_path' + + cache keys: business_path (simple) then path_set + + XXX-JPS PSEUDO CODE + """ + new_business_process = BusinessProcess() + accepted_path = [] + + explanation_cache = _getExplanationCache(explanation) + path_list = explanation_cache.getSimulationPathPatternList() + path_list = map(lambda x:x[0:-1], path_list) # Remove trailing % + path_set = set() + for simulation_movement in business_path.\ + _getExplanationRelatedSimulationMovementValueList(explanation): + simulation_path = simulation_movement.getPath() + for path in path_list: + if simulation_path.startswith(path): + path_set.add(path) # This selection path is part of explanation + + for business_path in self.getBusinessPathValueList(): + if business_path.hasMovementsIn(explanation, path_set): + accepted_path.append(business_path) + + new_business_process.addValueList(business_path) + return new_business_process + def _getExplanationCache(explanation): + if explanation.isinstance(ExplanationCache): + return explanation # Return cached value if any tv = getTransactionalVariable(explanation) if tv.get('explanation_cache', None) is None: tv['explanation_cache'] = ExplanationCache(explanation) return tv.get('explanation_cache') + +def _getBusinessPathClosure(explanation, business_path): + """Returns a + """ + explanation_cache = _getExplanationCache(explanation) + return explanation_cache.getBusinessPathClosure(business_path) diff --git a/product/ERP5/interfaces/business_path.py b/product/ERP5/interfaces/business_path.py index 15a7f67482..685e39f54c 100644 --- a/product/ERP5/interfaces/business_path.py +++ b/product/ERP5/interfaces/business_path.py @@ -59,6 +59,15 @@ class IBusinessPath(Interface): movement -- a Simulation Movement """ + def getCompletionDate(explanation): + """Returns the date of completion of the movemnet + based on paremeters of the business path. This completion date can be + the start date, the stop date, the date of a given workflow transition + on the explaining delivery, etc. + + XXXXXXXXXXXXXXXx + """ + def getExpectedQuantity(amount): """Returns the new quantity for the provided amount taking into account the efficiency or the quantity defined on the business path. @@ -116,6 +125,10 @@ class IBusinessPath(Interface): not yet completed (ex. in 'delivered' state). """ + def isDelivered(explanation): + """XXX + """ + def build(explanation): """Builds all related movements in the simulation using the builders defined on the Business Path diff --git a/product/ERP5/interfaces/business_process.py b/product/ERP5/interfaces/business_process.py index 9e8c7dfd45..722d284c6f 100644 --- a/product/ERP5/interfaces/business_process.py +++ b/product/ERP5/interfaces/business_process.py @@ -231,7 +231,7 @@ class ITradeStateProcess(Interface): implicitely defines a simulation subtree """ - def getLatestPartiallyCompletedTradeState(explanation): + def getLatestPartiallyCompletedTradeStateList(explanation): """Returns the list of completed trade states which predecessor states are completed and for which no successor state is partially completed in the context of given explanation. -- 2.30.9