From d487196a27f59ee92f2ba0147022668235349424 Mon Sep 17 00:00:00 2001 From: Jean-Paul Smets <jp@nexedi.com> Date: Sat, 22 May 2010 19:35:50 +0000 Subject: [PATCH] A first idea of how BusinessPath will look at the end (after garbage removal and refactoring). The ExplanationCache will be a central part of implemenation (and may need some interfaces). git-svn-id: https://svn.erp5.org/repos/public/erp5/sandbox/amount_generator@35546 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/BusinessPath.py | 370 +++++++++++++++----------- 1 file changed, 209 insertions(+), 161 deletions(-) diff --git a/product/ERP5/Document/BusinessPath.py b/product/ERP5/Document/BusinessPath.py index b474ee1a2b..39052eeba4 100644 --- a/product/ERP5/Document/BusinessPath.py +++ b/product/ERP5/Document/BusinessPath.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: shift_jis -*- ############################################################################## # # Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. @@ -37,6 +37,39 @@ from Products.ERP5.Document.Predicate import Predicate 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 @@ -81,9 +114,10 @@ class BusinessPath(Path, Predicate): , PropertySheet.Comment , PropertySheet.Arrow , PropertySheet.Amount - , PropertySheet.Chain + , PropertySheet.Chain # XXX-JPS Why N , PropertySheet.SortIndex , PropertySheet.BusinessPath + , PropertySheet.FlowCapacity , PropertySheet.Reference ) @@ -107,7 +141,7 @@ class BusinessPath(Path, Predicate): 'source_account', 'source_administration', #'source_advice', - #'source_carrier', + 'source_carrier', #'source_decision', 'source_function', 'source_payment', @@ -125,7 +159,7 @@ class BusinessPath(Path, Predicate): Returns all categories which are used to define the destination of this Arrow """ - # Naive implementation - we must use category groups instead - XXX + # Naive implementation - we must use category groups instead - XXX-JPS review this later return ('destination', 'destination_account', 'destination_administration', @@ -143,7 +177,7 @@ class BusinessPath(Path, Predicate): security.declareProtected(Permissions.AccessContentsInformation, 'getArrowCategoryDict') - def getArrowCategoryDict(self, context=None, **kw): + def getArrowCategoryDict(self, context=None, **kw): # XXX-JPS do we need it in API ? result = {} dynamic_category_list = self._getDynamicCategoryList(context) for base_category in self.getSourceArrowBaseCategoryList() +\ @@ -223,78 +257,59 @@ class BusinessPath(Path, Predicate): return method(context) return [] - # IBusinessBuildable implementation - def isBuildable(self, explanation): - """ - """ - # check if there is at least one simulation movement which is not - # delivered - result = False - if self.isCompleted(explanation) or self.isFrozen(explanation): - return False # No need to build what was already built or frozen - for simulation_movement in self.getRelatedSimulationMovementValueList( - explanation): - if simulation_movement.getDeliveryValue() is None: - result = True - predecessor = self.getPredecessorValue() - if predecessor is None: - return result - # 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 - if self.isStatePartiallyCompleted(explanation, predecessor): - return result - return False - - 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 + # IBusinessPath implementation + security.declareProtected(Permissions.AccessContentsInformation, + 'getMovementCompletionDate') + def getMovementCompletionDate(self, movement): + """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. + + movement -- a Simulation Movement + """ + method_id = self.getCompletionDateMethodId() + method = getattr(movement, method_id) # We wish to raise if it does not exist + return method() + + security.declareProtected(Permissions.AccessContentsInformation, + 'getExpectedQuantity') + def getExpectedQuantity(self, amount): + """Returns the new quantity for the provided amount taking + into account the efficiency or the quantity defined on the business path. + This is used to implement payment conditions or splitting production + over multiple path. The total of getExpectedQuantity for all business + path which are applicable should never exceed the original quantity. + The implementation of this validation is left to rules. """ - 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) - }) + if self.getQuantity(): + return self.getQuantity() + elif self.getEfficiency(): + return amount.getQuantity() * self.getEfficiency() + else: + return amount.getQuantity() - # 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 + """returns True if all related simulation movements for this explanation + document are in a simulation state which is considered as completed + according to the configuration of the current business path. + Completed means that it is possible to move to next step + of Business Process. This method does not check however whether previous + trade states of a given business process are completed or not. + Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose. + + explanation -- the Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree and a union + business process. + + NOTE: simulation movements can be completed (ex. in 'started' state) but + not yet frozen (ex. in 'delivered' state). """ acceptable_state_list = self.getCompletedStateList() - for movement in self.getRelatedSimulationMovementValueList(explanation): + for movement in self._getExplanationRelatedSimulationMovementValueList( + explanation): if movement.getSimulationState() not in acceptable_state_list: return False return True @@ -302,111 +317,71 @@ class BusinessPath(Path, Predicate): security.declareProtected(Permissions.AccessContentsInformation, 'isPartiallyCompleted') def isPartiallyCompleted(self, explanation): - """ - Looks at all simulation related movements - and checks the simulation_state of the delivery + """returns True if some related simulation movements for this explanation + document are in a simulation state which is considered as completed + according to the configuration of the current business path. + Completed means that it is possible to move to next step + of Business Process. This method does not check however whether previous + trade states of a given business process are completed or not. + Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose. + + explanation -- the Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree and a union + business process. """ acceptable_state_list = self.getCompletedStateList() - for movement in self.getRelatedSimulationMovementValueList(explanation): + for movement in self._getExplanationRelatedSimulationMovementValueList( + explanation): if movement.getSimulationState() in acceptable_state_list: return True return False - security.declareProtected(Permissions.AccessContentsInformation, - 'isFrozen') + 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 + """returns True if all related simulation movements for this explanation + document are in a simulation state which is considered as frozen + according to the configuration of the current business path. + Frozen means that simulation movement cannot be modified. + This method does not check however whether previous + trade states of a given business process are completed or not. + Use instead IBusinessPathProcess.isBusinessPathCompleted for this purpose. + + explanation -- the Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree and a union + business process. + + NOTE: simulation movements can be frozen (ex. in 'stopped' state) but + not yet completed (ex. in 'delivered' state). + """ + acceptable_state_list = self.getFrozenStateList() + movement_list = self._getExplanationRelatedSimulationMovementValueList( + explanation) + if not movement_list: + return False # Frozen is True only if some delivered movements exist for movement in movement_list: - if not movement.isFrozen(): + if movement.getDelivery() and movement.getSimulationState() not in acceptable_state_list: # XXX-JPS is it acceptable optimizatoin ? 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 - root_applied_rule_list = [] - delivery_simulation_movement_list = portal_catalog( - delivery_uid=[x.getUid() for x in explanation.getMovementList()]) - - for simulation_movement in delivery_simulation_movement_list: - applied_rule = simulation_movement.getRootAppliedRule().getPath() - if applied_rule not in root_applied_rule_list: - root_applied_rule_list.append(applied_rule) - - simulation_movement_list = portal_catalog( - portal_type='Simulation Movement', causality_uid=self.getUid(), - path=['%s/%%' % x for x in root_applied_rule_list]) - - return [simulation_movement.getObject() for simulation_movement - in simulation_movement_list - # related with explanation - if self._isDeliverySimulationMovementRelated( - simulation_movement, delivery_simulation_movement_list)] + def build(self, explanation): + """Builds all related movements in the simulation using the builders + defined on the Business Path. - def getExpectedQuantity(self, explanation, *args, **kwargs): + explanation -- the Order, Order Line, Delivery or Delivery Line which + implicitely defines a simulation subtree and a union + business process. """ - Returns the expected stop date for this - path based on the explanation. + builder_list = self.getDeliveryBuilderValueList() + explanation_cache = _getExplanationCache(explanation) + for builder in builder_list: + # Call build on each builder + # Provide 2 parameters: self and and explanation_cache + builder.build(select_method_dict={ + 'business_path': self, + 'explanation_cache': explanation_cache, + }) - XXX predecessor_quantity argument is required? - """ - if self.getQuantity(): - return self.getQuantity() - elif self.getEfficiency(): - return explanation.getQuantity() * self.getEfficiency() - else: - return explanation.getQuantity() + # XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX GARBAGE FROM HERE def getExpectedStartDate(self, explanation, predecessor_date=None, *args, **kwargs): """ @@ -527,3 +502,76 @@ class BusinessPath(Path, Predicate): else: return successor_expected_date + + + 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): # XXX-JPS not in API + """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): # XXX-JPS purpose ? NOT IN API + """ + 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 + root_applied_rule_list = [] + + + if getattr(self, 'getMovementList', None) is None: # XXX-JPS temp hack + return [] + + delivery_simulation_movement_list = portal_catalog( + delivery_uid=[x.getUid() for x in explanation.getMovementList()]) # XXX-JPS it seems explanation is not understood as it should - only the root + + for simulation_movement in delivery_simulation_movement_list: + applied_rule = simulation_movement.getRootAppliedRule().getPath() + if applied_rule not in root_applied_rule_list: + root_applied_rule_list.append(applied_rule) + + simulation_movement_list = portal_catalog( + portal_type='Simulation Movement', causality_uid=self.getUid(), + path=['%s/%%' % x for x in root_applied_rule_list]) + + return [simulation_movement.getObject() for simulation_movement + in simulation_movement_list + # related with explanation + if self._isDeliverySimulationMovementRelated( + simulation_movement, delivery_simulation_movement_list)] -- 2.30.9