############################################################################## # # Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@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. # ############################################################################## from Globals import InitializeClass, PersistentMapping from AccessControl import ClassSecurityInfo from Products.CMFCore.PortalFolder import ContentFilter from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface from Products.ERP5.Document.Path import Path import zope.interface class BusinessPath(Path): """ The BusinessPath class embeds all information related to lead times and parties involved at a give phase of a business process. BusinessPath are also used as helper to build buildable movements. Here is the typical code of an alarm: Approach 1: explanation per explanation builder = portal_deliveries.default_order_builder for path in builder.getSpecialiseRelatedValueList() # or wharever category for explanation in portal_catalog(buildable=1, portal_type='Order'): path.build(explanation) Pros: easy explanation based approach Cons: buildable column added in delivery table reexpand of a finished order might generate remaining buildable Approach 2: isBuildable is indexed for SimulationMovements isBuildable() method is added to SimulationMovement Pros: global select is possible Cons: reindex of simulation is required slow indexing Approach 3: isBuildable is invoked during build "after select" process builder = portal_deliveries.default_order_builder for path in builder.getSpecialiseRelatedValueList() # or wharever category builder.build(causality_uid=path.getUid(),) # Select movemenents Pros: global select is possible Cons: global select retrieves long lists slow build Method 3 is best """ meta_type = 'ERP5 Business Path' portal_type = 'Business Path' isPredicate = 1 # 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.BusinessPath ) # Declarative interfaces zope.interface.implements(Interface.ICategoryAccessProvider, Interface.IArrow) # IBusinessPath Interface security.declareProtected(Permissions.AccessContentsInformation, 'getSourceBaseCategoryList') def getSourceBaseCategoryList(self): """ Returns all categories which are used to define the source of this Arrow """ # Naive implementation - we must use category groups instead return ('source', 'source_section', 'source_payment', 'source_project', ) security.declareProtected(Permissions.AccessContentsInformation, 'getDestinationBaseCategoryList') def getDestinationBaseCategoryList(self): """ Returns all categories which are used to define the destination of this Arrow """ # Naive implementation - we must use category groups instead return ('destination', 'destination_section', 'destination_payment', 'destination_project', ) # ICategoryAccessProvider overriden methods def _getCategoryMembershipList(self, category, **kw): """ Overriden in order to take into account dynamic arrow categories """ context = kw.get('context') result = Path._getCategoryMembershipList(self, category, **kw) if context is not None: dynamic_category_list = self._getDynamicCategoryList(context) dynamic_category_list= self._filterCategoryList(dynamic_category_list, category, **kw) # TODO: static categories should have priority over dynamic categories result = dynamic_category_list + result return result def _getAcquiredCategoryMembershipList(self, category, **kw): """ Overriden in order to take into account dynamic arrow categories """ context = kw.pop('context', None) result = Path._getAcquiredCategoryMembershipList(self, category, **kw) if context is not None: dynamic_category_list = self._getDynamicCategoryList(context) dynamic_category_list= self._filterCategoryList(dynamic_category_list, category, **kw) # TODO: static categories should have priority over dynamic categories result = dynamic_category_list + result 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 """ return category_list # 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 [] # Core API def isBuildable(self, explanation): """ """ if self.isCompleted(explanation): return False # No need to build what was already built if self.isFrozen(explanation): return False # No need to build what is frozen predecessor = self.getPredecessorValue() if predecessor is None: return True # No predecessor, let's build if predecessor.isCompleted(explanation): return True return False def isPartiallyBuildable(self, explanation): """ Not sure if this will exist some day XXX """ def _getRelatedSimulationMovementList(self, explanation): """ """ return self.getCausalityRelatedValueList(portal_type='Simulation Movement', explanation_uid=explanation.getUid()) 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._getRelatedSimulationMovementList(explanation): if movement.getSimulationState() not in acceptable_state_list: return False return True 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._getRelatedSimulationMovementList(explanation): if movement.getSimulationState() in acceptable_state_list: return True return False def isFrozen(self, explanation): """ Looks at all simulation related movements and checks if frozen """ movement_list = self._getRelatedSimulationMovementList(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 build(self, explanation): """ Build """ builder_list = self.getBuilderList() # Missing method for builder in builder_list: builder.build(causality_uid=self.getUid()) # This is one way of doing builder.build(movement_relative_url_list= self._getRelatedSimulationMovementList(explanation)) # Another way # Date calculation 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